From 78c303990638118c48e5d11c05df533f19e896af Mon Sep 17 00:00:00 2001 From: Yao Jingxi <23722032@bjtu.edu.cn> Date: Fri, 13 Nov 2026 23:30:36 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat(core)!:=20=E5=88=A0=E9=99=A4=20Java/Ma?= =?UTF-8?q?ven/JDT=20=E7=89=B9=E5=88=A4=E4=B8=8E=E5=A5=91=E7=BA=A6?= =?UTF-8?q?=E4=B8=93=E5=B1=9E=E6=AD=BB=E5=91=BD=E4=BB=A4=EF=BC=8Crun/start?= =?UTF-8?q?=20=E4=B8=B4=E6=97=B6=E6=96=87=E4=BB=B6=E6=94=AF=E6=8C=81=20bas?= =?UTF-8?q?e64?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Core 对 Java 等语言零依赖、零特判,语言和工具专属知识全部由插件注入。 - 删除 execution.planLaunchCommand(JDK @argfile 规划)及 execution/launch_plan.rs - 删除 LSP 内置语言目录:lsp/languages/catalog.rs、resources/lsp/language-providers.json、 FFI lithe_core_lsp_provider_catalog_json 与头文件声明 - 删除 tree-sitter、tree-sitter-java 依赖 - 删除 lithe-ide-host 目录里的 Java/Maven 工具 - 删除只存在于契约、fixture、测试中的 23 个命令名(java.*、lsp.jdt*、maven.*、 mybatis.index、spring.index、runConfig.selectJava、build.languageImport、 agent.attachments.retentionPlan)及其 fixture - 删除 maven-launch-context-v1、maven-portable-configuration-v1、run-configuration-v1 schema - run/start 的 temporaryFiles 增加 {name, base64} 字节文件,宿主原样写入 - 模板 ${field..major} 取第一个数字分量,不再特判 1.x - 旧版工具链 pin 的条目改为不透明字符串记录,不再点名 javaHome - 契约文档和测试 fixture 去掉 Java/Maven/JDT 专属内容 Refs 1lck/Lithe-IDEA#1856, 1lck/Lithe-IDEA#1857, 1lck/Lithe-IDEA#1858, 1lck/Lithe-IDEA#1853 --- rust/Cargo.lock | 38 - rust/contracts/application-boundary.md | 372 +----- rust/contracts/ide-api/v1.md | 44 +- .../maven-launch-context-v1.schema.json | 21 - ...aven-portable-configuration-v1.schema.json | 22 - .../run-configuration-v1.schema.json | 48 - .../run-configuration-v2.schema.json | 43 +- rust/contracts/rust-core-api.md | 1157 ++--------------- rust/contracts/toolchain-local-v1.schema.json | 29 +- .../toolchain-requirements-v1.schema.json | 4 +- .../shared-defaults.json | 2 +- .../build-project-options/validation.json | 26 +- rust/fixtures/debug/hot-code-replace-v1.json | 2 +- .../editor/basic-file-highlighting-v1.json | 40 - .../execution/framework-identity.json | 114 -- rust/fixtures/execution/gradle.json | 92 -- .../maven-java-main-source-sets-v1.json | 28 - .../execution/maven-runtime-versions.json | 10 - rust/fixtures/execution/maven.json | 245 ---- .../standalone-java-compile-run-v1.json | 40 - rust/fixtures/ide-api/calls.json | 49 +- rust/fixtures/ide-api/host-tool-exposure.json | 18 - rust/fixtures/java/basic.json | 21 - rust/fixtures/java/run-markers-v1.json | 216 --- rust/fixtures/lsp/java-build-report-v1.json | 31 - rust/fixtures/lsp/java-completion-v1.json | 23 - .../lsp/java-launch-preparation-v1.json | 80 -- rust/fixtures/lsp/java-navigation-v1.json | 58 - .../lsp/java-project-runtimes-v1.json | 78 -- .../lsp/java-workspace-policy-v1.json | 79 -- rust/fixtures/lsp/jdt-cache-retention-v1.json | 52 - rust/fixtures/lsp/jdt-direct-launch-v1.json | 34 - rust/fixtures/lsp/jdt-readiness-v1.json | 74 -- .../lsp/jdt-workspace-fingerprint-v1.json | 35 - rust/fixtures/lsp/jdt-workspace-key-v1.json | 17 - .../fixtures/lsp/maven-profile-events-v1.json | 11 - rust/fixtures/lsp/project-preparation-v1.json | 12 - rust/fixtures/maven/basic.json | 49 - rust/fixtures/maven/dependency-tree-v2.json | 140 -- rust/fixtures/maven/diagnostics.json | 28 - rust/fixtures/maven/launch-plan-v1.json | 88 -- rust/fixtures/maven/platform-contract-v1.json | 30 - rust/fixtures/maven/source-roots-v1.json | 57 - rust/fixtures/maven/test-results-v1.json | 304 ----- .../fixtures/maven/testcase-durations-v1.json | 72 - rust/fixtures/modules/built-in-v1.json | 1 - rust/fixtures/mybatis/basic.json | 25 - .../lithe-spring-boot-git-graph/.classpath | 57 - .../lithe-spring-boot-git-graph/.project | 34 - .../org.eclipse.core.resources.prefs | 4 - .../.settings/org.eclipse.jdt.apt.core.prefs | 2 - .../.settings/org.eclipse.jdt.core.prefs | 10 - .../.settings/org.eclipse.m2e.core.prefs | 4 - .../lithe-spring-boot-git-graph/README.md | 17 - .../history/OrderController-feature.java | 14 - .../history/UserService-feature.java | 18 - .../lithe-spring-boot-git-graph/pom.xml | 46 - .../com/example/demo/DemoApplication.java | 11 - .../com/example/demo/user/UserController.java | 27 - .../com/example/demo/user/UserRepository.java | 17 - .../com/example/demo/user/UserService.java | 18 - .../src/main/resources/application.yml | 5 - .../automatic-java-selection.json | 284 ---- rust/fixtures/run-configuration/basic.json | 23 - .../run-configuration/build-provider.json | 4 +- .../configuration-toolchain-selection.json | 446 ------- .../run-configuration/editor-save.json | 20 +- .../run-configuration/hybrid-service-web.json | 58 + .../run-configuration/hybrid-spring-vue.json | 43 - .../run-configuration/invalid-version.json | 7 - .../maven-module-ownership.json | 91 -- .../run-configuration/multi-module.json | 39 - rust/fixtures/run-configuration/no-entry.json | 7 - .../project-environment.json | 22 - .../project-toolchain-selection.json | 328 ----- .../provider-prepared-java.json | 17 - .../provider-project-ownership.json | 156 --- .../run-configuration/single-module.json | 23 - .../subproject-toolchains.json | 45 - .../run-configuration/toolchain-mismatch.json | 15 - rust/fixtures/spring/basic.json | 45 - rust/lithe-core/Cargo.toml | 2 - rust/lithe-core/include/lithe_core.h | 1 - .../resources/lsp/language-providers.json | 500 ------- rust/lithe-core/src/build/run_start.rs | 63 +- rust/lithe-core/src/debug/engine.rs | 4 +- rust/lithe-core/src/debug/types.rs | 2 +- rust/lithe-core/src/execution/launch_plan.rs | 451 ------- rust/lithe-core/src/execution/mod.rs | 6 - rust/lithe-core/src/lsp/interface/client.rs | 10 +- rust/lithe-core/src/lsp/interface/engine.rs | 16 +- rust/lithe-core/src/lsp/interface/process.rs | 8 +- rust/lithe-core/src/lsp/languages/catalog.rs | 392 ------ rust/lithe-core/src/lsp/languages/mod.rs | 3 - rust/lithe-core/src/lsp/mod.rs | 1 - rust/lithe-core/src/lsp/tests.rs | 252 ---- rust/lithe-core/src/plugins/declarative.rs | 4 +- .../src/plugins/web_content_tests.rs | 4 +- .../src/project_generators/archive.rs | 4 +- .../lithe-core/src/project_generators/form.rs | 33 +- .../src/project_generators/manifest.rs | 4 +- rust/lithe-core/src/project_generators/mod.rs | 4 +- .../src/project_generators/template.rs | 38 +- rust/lithe-core/src/protocol/command.rs | 3 - rust/lithe-core/src/runtime/dispatcher.rs | 16 - rust/lithe-core/src/runtime/ffi.rs | 27 - .../src/tests/build_project_options.rs | 4 +- rust/lithe-core/src/tests/plugin_run_debug.rs | 64 + .../lithe-core/src/tests/run_configuration.rs | 156 +-- rust/lithe-core/src/tests/toolchain_pins.rs | 11 +- rust/lithe-core/src/toolchain/pins.rs | 10 +- rust/lithe-ide-host/src/catalog.rs | 84 +- rust/lithe-ide-host/src/lib.rs | 23 +- shared/plugins/contract/v1/README.md | 117 +- .../contract/v1/plugin-manifest.schema.json | 15 +- .../plugins/contract/v1/toolchains/README.md | 4 +- .../plugins/guides/build-tool-integration.md | 39 +- shared/plugins/reference/api-catalog.md | 4 +- shared/plugins/reference/domains/backend.md | 4 +- .../reference/domains/build-systems.md | 12 +- shared/plugins/reference/domains/languages.md | 42 +- shared/plugins/reference/domains/manifest.md | 8 +- .../plugins/reference/protocol-changelog.md | 7 + 123 files changed, 670 insertions(+), 7798 deletions(-) delete mode 100644 rust/contracts/maven-launch-context-v1.schema.json delete mode 100644 rust/contracts/maven-portable-configuration-v1.schema.json delete mode 100644 rust/contracts/run-configuration-v1.schema.json delete mode 100644 rust/fixtures/editor/basic-file-highlighting-v1.json delete mode 100644 rust/fixtures/execution/framework-identity.json delete mode 100644 rust/fixtures/execution/gradle.json delete mode 100644 rust/fixtures/execution/maven-java-main-source-sets-v1.json delete mode 100644 rust/fixtures/execution/maven-runtime-versions.json delete mode 100644 rust/fixtures/execution/maven.json delete mode 100644 rust/fixtures/execution/standalone-java-compile-run-v1.json delete mode 100644 rust/fixtures/java/basic.json delete mode 100644 rust/fixtures/java/run-markers-v1.json delete mode 100644 rust/fixtures/lsp/java-build-report-v1.json delete mode 100644 rust/fixtures/lsp/java-completion-v1.json delete mode 100644 rust/fixtures/lsp/java-launch-preparation-v1.json delete mode 100644 rust/fixtures/lsp/java-navigation-v1.json delete mode 100644 rust/fixtures/lsp/java-project-runtimes-v1.json delete mode 100644 rust/fixtures/lsp/java-workspace-policy-v1.json delete mode 100644 rust/fixtures/lsp/jdt-cache-retention-v1.json delete mode 100644 rust/fixtures/lsp/jdt-direct-launch-v1.json delete mode 100644 rust/fixtures/lsp/jdt-readiness-v1.json delete mode 100644 rust/fixtures/lsp/jdt-workspace-fingerprint-v1.json delete mode 100644 rust/fixtures/lsp/jdt-workspace-key-v1.json delete mode 100644 rust/fixtures/lsp/maven-profile-events-v1.json delete mode 100644 rust/fixtures/lsp/project-preparation-v1.json delete mode 100644 rust/fixtures/maven/basic.json delete mode 100644 rust/fixtures/maven/dependency-tree-v2.json delete mode 100644 rust/fixtures/maven/diagnostics.json delete mode 100644 rust/fixtures/maven/launch-plan-v1.json delete mode 100644 rust/fixtures/maven/platform-contract-v1.json delete mode 100644 rust/fixtures/maven/source-roots-v1.json delete mode 100644 rust/fixtures/maven/test-results-v1.json delete mode 100644 rust/fixtures/maven/testcase-durations-v1.json delete mode 100644 rust/fixtures/mybatis/basic.json delete mode 100644 rust/fixtures/projects/lithe-spring-boot-git-graph/.classpath delete mode 100644 rust/fixtures/projects/lithe-spring-boot-git-graph/.project delete mode 100644 rust/fixtures/projects/lithe-spring-boot-git-graph/.settings/org.eclipse.core.resources.prefs delete mode 100644 rust/fixtures/projects/lithe-spring-boot-git-graph/.settings/org.eclipse.jdt.apt.core.prefs delete mode 100644 rust/fixtures/projects/lithe-spring-boot-git-graph/.settings/org.eclipse.jdt.core.prefs delete mode 100644 rust/fixtures/projects/lithe-spring-boot-git-graph/.settings/org.eclipse.m2e.core.prefs delete mode 100644 rust/fixtures/projects/lithe-spring-boot-git-graph/README.md delete mode 100644 rust/fixtures/projects/lithe-spring-boot-git-graph/history/OrderController-feature.java delete mode 100644 rust/fixtures/projects/lithe-spring-boot-git-graph/history/UserService-feature.java delete mode 100644 rust/fixtures/projects/lithe-spring-boot-git-graph/pom.xml delete mode 100644 rust/fixtures/projects/lithe-spring-boot-git-graph/src/main/java/com/example/demo/DemoApplication.java delete mode 100644 rust/fixtures/projects/lithe-spring-boot-git-graph/src/main/java/com/example/demo/user/UserController.java delete mode 100644 rust/fixtures/projects/lithe-spring-boot-git-graph/src/main/java/com/example/demo/user/UserRepository.java delete mode 100644 rust/fixtures/projects/lithe-spring-boot-git-graph/src/main/java/com/example/demo/user/UserService.java delete mode 100644 rust/fixtures/projects/lithe-spring-boot-git-graph/src/main/resources/application.yml delete mode 100644 rust/fixtures/run-configuration/automatic-java-selection.json delete mode 100644 rust/fixtures/run-configuration/basic.json delete mode 100644 rust/fixtures/run-configuration/configuration-toolchain-selection.json create mode 100644 rust/fixtures/run-configuration/hybrid-service-web.json delete mode 100644 rust/fixtures/run-configuration/hybrid-spring-vue.json delete mode 100644 rust/fixtures/run-configuration/invalid-version.json delete mode 100644 rust/fixtures/run-configuration/maven-module-ownership.json delete mode 100644 rust/fixtures/run-configuration/multi-module.json delete mode 100644 rust/fixtures/run-configuration/no-entry.json delete mode 100644 rust/fixtures/run-configuration/project-environment.json delete mode 100644 rust/fixtures/run-configuration/project-toolchain-selection.json delete mode 100644 rust/fixtures/run-configuration/provider-prepared-java.json delete mode 100644 rust/fixtures/run-configuration/provider-project-ownership.json delete mode 100644 rust/fixtures/run-configuration/single-module.json delete mode 100644 rust/fixtures/run-configuration/subproject-toolchains.json delete mode 100644 rust/fixtures/run-configuration/toolchain-mismatch.json delete mode 100644 rust/fixtures/spring/basic.json delete mode 100644 rust/lithe-core/resources/lsp/language-providers.json delete mode 100644 rust/lithe-core/src/execution/launch_plan.rs delete mode 100644 rust/lithe-core/src/lsp/languages/catalog.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 961c7b5a..a69e7b78 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -978,8 +978,6 @@ dependencies = [ "similar", "toml", "toml_edit", - "tree-sitter", - "tree-sitter-java", "unicode-normalization", "url", "zip", @@ -1947,12 +1945,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" - [[package]] name = "string_cache" version = "0.9.0" @@ -2276,36 +2268,6 @@ dependencies = [ "windows", ] -[[package]] -name = "tree-sitter" -version = "0.25.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87" -dependencies = [ - "cc", - "regex", - "regex-syntax", - "serde_json", - "streaming-iterator", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-java" -version = "0.23.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa6cbcdc8c679b214e616fd3300da67da0e492e066df01bcf5a5921a71e90d6" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-language" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782" - [[package]] name = "try-lock" version = "0.2.5" diff --git a/rust/contracts/application-boundary.md b/rust/contracts/application-boundary.md index fd8a0d00..5d41ea90 100644 --- a/rust/contracts/application-boundary.md +++ b/rust/contracts/application-boundary.md @@ -40,130 +40,11 @@ Core's stateless `search.actions` returns ordered candidate offsets; the host ke ownership of action availability, execution and module activation. See [the action search fixture](../fixtures/search/actions-v1.json). -Global Agent instructions edit each upstream Agent's native user file, independently -of the current project and provider. Opening settings is read-only. Codex uses -`CODEX_HOME` (otherwise the user home `.codex`) and its first non-empty -`AGENTS.override.md` / `AGENTS.md`; Claude uses `CLAUDE_CONFIG_DIR` (otherwise -the user home `.claude`) and `CLAUDE.md`. The host resolves these locations; -UI supplies no arbitrary read/write destination. Absolute native paths in snapshots -are display and optimistic-write guards, not portable identifiers. A save checks -the snapshot's path and revision, replaces the file atomically, and reads the -effective source back. Failures retain the draft; reloading explicitly discards it -only after a successful read. Drafts stay separate by Agent. Updated instructions -load in a new native conversation; existing sessions are not rewritten or stopped. -Project instructions retain the upstream Agent's precedence rules. See -`fixtures/agent/agent-instructions-v1.json`. - -A user can also author one shared instruction text in platform-owned application -support storage and opt each supported Agent into a delimited block in its native -file. Synchronization preserves every byte outside that block. Turning a switch -off removes only that block. Enabled, unknown and out-of-date states come from -native snapshots, not a stored preference. Source saves and each target replacement -are atomic individually, not a cross-file transaction: per-Agent conflicts remain -visible and retryable while the saved shared draft and successful targets survive. -No startup/background synchronization or extra prompt injection occurs. A malformed -or duplicate marker is never repaired by overwriting it automatically. See -`fixtures/agent/shared-instructions-v1.json`. - Shared Monaco highlighting is owned by the current view/model pair. A queued callback for a closed or replaced model must finish without running against the new model or locking surviving documents. Primary, split and diff views share this lifecycle protection; actual symbol computation remains upstream-owned. -Agent prompt completion may include Agent-reported token counters; their accounting -scope belongs to the provider and is separate from context occupancy and subscription -quota. Platforms may measure locally observed turns with a monotonic clock, including -tools and permission waits, and freeze elapsed time at completion, failure or disconnect. -Normal Agent turns and permission decisions have no total-duration deadline. Quiet -activity notices are advisory and retain the busy turn; stop remains acknowledged -with bounded process cleanup. Missing usage and unmeasured replayed history remain -unknown. See the -[Agent host contract](rust-core-api.md) and `fixtures/agent/acp-events-v1.json`. - -Agent history lists can expose matching summaries as pages arrive while the remaining scan stays loading. Partial results prove presence only; final snapshots reconcile removals, and later failures retain visible rows. History surfaces share connection-local recent snapshots and in-flight requests; manual refresh remains immediate. Workspace/Agent isolation, stale-token rejection and upstream history ownership apply to both partial and final results. - -History provider filtering defaults to all providers and uses only upstream-reported -`modelProvider` identities. Choices span active and archived records; missing metadata -has an explicit unknown choice. A provider selection composes with title/ID search, -favorites and local removal, including archive results. It is transient presentation -state, not a routing or account setting, and never narrows the upstream history scan -or infers provenance from the active model. Bulk actions remain limited to visible -eligible records. Empty filtered results do not establish that no history exists. - -An Agent that advertises steering can accept supplementary user input while its -logical turn is active. Input delivery has a separate pending/received/failed -outcome and does not replace turn ownership, permissions, timing, or Stop. -Only an explicit unconsumed-input acknowledgement permits a normal continuation -prompt; slash commands are explicitly left unconsumed and deferred to this owned -prompt path, whose response also completes command-only input. Pending -continuations remain cancellable while staged configuration is awaiting its -acknowledgement, without cancelling the configuration itself. Unknown delivery -is never retried automatically. Platform UI enables -this behavior from the negotiated capability, not the Agent's display name. - -File context actions target a configured Agent and a root conversation within the -captured workspace. The quick action names the current Agent; other destinations -are grouped by Agent and identify existing sessions or a new conversation. Opening -the menu uses cached session summaries and does not start background Agents. -Attaching appends deduplicated native file references to that destination's draft, -preserves its text, and never sends, steers or cancels a turn. Draft text and files -remain isolated by Agent/session, including new-session creation and failed history -loads. Stale projects, removed Agents and unavailable sessions must not redirect the -attachment. Directories are not recursively expanded. References read saved disk -contents; dirty editor buffers are retained and prompt the user to save before sending. - -Agent activity separates reported plans, native subagents and file edits. Ordinary -tools are execution evidence, never inferred plan steps or child agents. Native -children expose their upstream task, state and delivered output, scoped to their -root conversation. Completion counts only explicit success; unknown outcomes -remain disconnected. Child permissions use the owning root's existing controls. -A platform enables this monitor only when it consumes negotiated child-session -events; execution and history storage remain owned by the upstream Agent. - -The opened workspace, supplied by the product, defines the default meaning of -"current project". A containing Git repository and inherited development rules -do not replace that scope. The shared Host adds the same explicit workspace -context to ordinary prompts and supplementary messages, including prompts after -history restoration. File contents and applicable rules remain Agent-owned; -explicit user requests can select a different scope. Native slash commands retain -their original text and ordered attachments, including deferred supplementary -commands; attachments do not cause workspace context to be appended. Host-added context is omitted from user-message replay -presented by Lithe and from generated live/history-list titles, without rewriting -upstream history or filtering model replies. Titles left empty by this filtering -are missing values, so presentation retains its existing title fallbacks. - -The Host enables the native Codex checklist through process-local -`tools.update_plan.enabled` configuration for both API and subscription launches. -It does not edit CLI settings or switch collaboration mode. Plan creation and -updates remain Agent-owned. - -Native child permissions preserve their requester separately from the root turn. -An ended child or ancestor cancels only that subtree's pending permissions; -presentation consumes the same lifecycle update to remove its prompts and refresh -attention/timing. Root and sibling permissions and the root prompt stay active. - -Agent activity presentation uses the selected session's reported plan, native -subagents and file diffs. Compact summaries may omit empty categories; ordinary -tools do not create placeholder tasks or child sessions. File-summary line totals -require complete reported counts for all included, completed changes; missing or -overflowing counts remain unknown rather than becoming partial or estimated totals. -Review acknowledgements are local to the exact reported version; later edits remain reviewable and start after the acknowledged prefix. -The same open session retains its acknowledgements through disconnects and history -reloads; closing its tab releases them. Replay must still match the exact version -or prefix, so changed upstream evidence is never hidden by a stale acknowledgement. -Reported excerpts and missing original content are not full-file snapshots. -Native rollback is an explicit user action: preserve dirty editor buffers, -reject paths outside the workspace and ambiguous or incomplete evidence, retain -unrelated text, and use guarded native writes preserving encoding. Creation -requires explicit evidence before recoverable removal. Reject replacement terminal -symlinks instead of following them. Guard removal with the expected byte identity -and the native save lock; verify the actual moved object, restore it on conflict -without overwriting a recreated path, and report any manual recovery location. -Unsupported adapters must reject removal rather than use unchecked Trash. -Restoring a deletion -requires the path to remain absent. Batch failures retain unprocessed changes. -These presentation actions do not add Agent Host commands or persist chat history. - | Feature | Shared input/output | Platform-owned implementation | | --- | --- | --- | | Workspace | visible snapshot, relative paths, file metadata, deterministic ordering | workspace root selection, native dialogs, and watchers | @@ -172,11 +53,9 @@ These presentation actions do not add Agent Host commands or persist chat histor | Git | workspace commit plans with optional immutable ChangeList path scopes, dependency ordering, guarded steps and partial-success retry, changes, commits, branches, diffs, reviewed history actions and recovery, worktree listing and safe management, worktree-aware PR publication context, validation, and mutation results | Git executable discovery, credentials, process environment, opening checkout paths, local ChangeList selection and preference persistence | | GitHub | remote parsing, trusted request plans, normalized branch comparisons and pull requests/reviews/comments, deterministic ordering, and stable errors | OAuth configuration, HTTPS, browser opening, and operating-system credential storage | | [AI commit messages](ai-commit.md) | provider configuration parsing, commit rules, bounded diff evidence, request plans, and response text | local configuration discovery, credentials, HTTP, cancellation, and draft UI | -| Agent conversation (ACP) | supported-agent catalog, Node.js/npm detection, adapter install with the user's npm and numbers-only live download progress, CLI provenance and owner-preserving updates, per-agent API-key and model delivery over ACP stdio (Codex gateway authentication; Claude public session options), explicit Codex subscription authentication via the installed CLI, bounded official App Server quota reads with account checks, ACP v1 connection per workspace and agent, agent-owned session history (list/load), session config options, user-selected file references as ACP resource links, streamed tool evidence, permission decisions, acknowledged cancellation with bounded recovery, and process-tree lifecycle in the shared Rust host | provider and agent settings, API-key storage, credential-independent local default-model reading through AI configuration ports, data directory, workspace selection, module enablement, conversation UI and compact subscription quota presentation, workspace/Agent-scoped local history annotations (favorites, title overrides and recoverable hidden rows), and user-selected Markdown export destinations | | [IDE capability API / MCP](ide-api/v1.md) | canonical tool schemas, narrowing host selection, permission/argument validation, stable execution IDs, bounded output cursors and errors | implemented workflow inventory, explicit project authorization, live environment/Build/Run application actions, helper discovery and writable connection storage | -| Runtime | Java/Maven requirements, normalized candidates, and effective toolchain references | JDK/Maven probing and executable paths | +| Runtime | tool requirements, normalized candidates, and effective toolchain references | tool probing and executable paths | | Language tooling | provider catalog, local fallback results, complete LSP process/session runtime, capabilities, diagnostics, UTF-16 edits, and normalized feature results | executable/environment discovery and UI provider routing | -| Java/Maven/Spring | deterministic Maven-root selection, project structure, modules and profiles, bounded dependency-tree normalization; compiler diagnostic parsing; Java source structure, symbols, code vision, run-configuration detection, Spring configuration/bean/endpoint indexing, and JDTLS/Java Debug adapter policy | JDK/Maven discovery, local dependency-repository selection, Java/Maven child processes, and sockets | | Run/Debug | versioned configuration documents, three-layer resolution, diagnostics, platform-neutral launch plans, DAP framing/state, reverse terminal requests, breakpoint relocation, stepping filters, threads, stacks, variables, and events | project and preference persistence, native edit reporting, adapter discovery, PTY/ConPTY debuggee launch, child processes, sockets, native termination, and UI | | Terminal | input bytes, output bytes, lifecycle; [shell selection semantics](terminal-profiles.md) | PTY/ConPTY, shell discovery, native profile preferences, and environment | | Workbench background | versioned source (`none`, bundled slot `01`–`10`, or `custom`) and opacity | UI, image rendering, bundled-resource packaging, local-image access permission and persistence | @@ -218,11 +97,11 @@ marker. Existing Dev data is never merged or overwritten; subsequent launches do not fall back to Stable. Changing the channel alone does not switch profiles. Updater/channel suppression state, custom log destinations, browser caches, indexes, locks and runtime connections are excluded. macOS also copies its local -AI secret file, Maven preferences and marketplace source configuration, along with +AI secret file and marketplace source configuration, along with persisted database configuration/history/schedules and custom background image access records. These preference keys also count as existing Dev data. OS-vault credentials remain separate and may require signing in again. Windows snapshots -only native settings/Maven JSON and closed WebView localStorage, before creating +only native settings JSON and closed WebView localStorage, before creating any WebView; it never copies a live browser profile. A busy source or failed copy stops initialization with a retry message and no success marker. Published files and preferences resume from a staged snapshot after an interrupted commit. Stable @@ -338,11 +217,11 @@ contract must preserve these invariants: - Native plugin factories receive a read-only host context. Host services use stable IDs and shared protocols; plugin code cannot import a platform composition root or the application executable. -- AI Assistance, Agent Conversation, Terminal, Git, Search, Local History, Debug, and Java/Maven - execution are built-in lifecycle modules. They are not marketplace plugins. -- Java language tooling remains part of the built-in product. Every other - language provider is represented by an independently configurable bundled - language-support plugin. On macOS, Go and PHP are community declarative +- AI Assistance, Terminal, Git, Search, Local History, and Debug are built-in + lifecycle modules. They are not marketplace plugins. +- Every language provider, build tool and ecosystem runner is represented by an + independently configurable language-support plugin; Core names none of them. + On macOS, Go and PHP are community declarative plugins from Lithe Store (`io.github.lithe-idea.go-support`, `io.github.lithe-idea.php-support`); the retired official IDs `dev.lithe.plugin.go-support` and `dev.lithe.plugin.php-support` keep their @@ -427,64 +306,10 @@ exact launched root, never a process selected by name. Consumers must validate `operationID` before retaining it and invalidate it on stop, completion, reset, or replacement. Hosts without a local PID leave it absent. Test performance uses native CPU/RSS of that root and its descendants (including forked workers), with -RSS explicitly labeled as resident memory rather than JVM heap. Debug keeps its -existing exact-JVM JDK heap sampling; these are separate metric sources. - -Java workspace activation is a shared Core decision over visible relative -paths. Any workspace containing a non-ignored `.java` source starts one JDT LS -session asynchronously, even without Maven or Gradle metadata. That session is -owned by the workspace and remains alive until the workspace closes or the user -explicitly restarts it. `ready` means JDT LS has completed project import, not -merely that the process answered `initialize`. - -JDT LS runs only on the Temurin JDK 21 bundled with Lithe. This runtime is -independent of project Run/Debug JDK selection and has no user-configurable -path. A missing or invalid bundle is a packaging failure. The application shows -preparing, ready, failure, and timeout notifications; a navigation command while -preparing ends after the notice and is never replayed later. - -macOS and Windows adapters discover the selected JDT LS installation's Equinox -launcher JAR, platform configuration directory, Lombok agent, Java Debug -Server, and bundled Java executable. Java Test-capable adapters additionally -submit ordered extension bundles; Rust Core owns their ordering and -de-duplication, the JVM flags, and direct `java`/`java.exe` startup with array -arguments. The macOS TestNG runner remains a packaged native resource used only -when a TestNG session starts. Packaged JDT LS therefore has no runtime dependency -on shell wrappers, PowerShell, or the user's `PATH`. Legacy wrappers are an -external-plan compatibility fallback and are not the packaged execution path. - -Java test discovery remains a language-service workflow rather than a UI or -Debug Core parser. When the Tests tool window is opened or refreshed, the -language facade offers every Java source to Core's typed `javaTestItems` -operation; Core asks the Java Test extension for the class and method tree and -platforms project stable fully qualified identifiers into the native list. -Neither file names nor locally recognized annotations may prefilter this request, -so inherited tests and custom composed annotations remain visible. Closing the tool window, changing workspace, or reloading the -Java runtime cancels the owning discovery operation; late results cannot replace -the current workspace's tree. Discovery does not create a Debug session, result -socket, adapter connection, or target JVM. - -Starting one JUnit or TestNG file, class, or method creates a short-lived native -loopback result listener on demand. JDT LS owns project/test metadata, Rust Core -owns deterministic DAP launch argument projection, and the Debug module owns the -adapter session. Repeated launch, stop, project close, runtime reload, and launch -failure all cancel the active operation and release the listener. The selected -Run configuration remains the source of project-scoped Java runtime selection; -JDT LS remains authoritative for the test runner classpath, working directory, -and test-specific VM and program arguments. - -Platforms observe JDT LS version and non-recursive build-file metadata, while -Rust Core alone validates and reduces those observations to the opaque workspace -fingerprint. macOS and Windows adapters must not duplicate its ordering, -de-duplication, or serialization rules. +RSS explicitly labeled as resident memory rather than a runtime's managed heap. Editor changes use versioned incremental document synchronization and do not -restart the session or clear its index. Workspace watchers coalesce Maven and -Gradle changes before one project refresh and publish normalized watched-file -events. Platform adapters mark selected JDT LS caches as recently used, ask -Rust Core which inactive caches exceed the 30-day retention period, and delete -only those validated directories. **Java: Rebuild Index** remains the explicit -recovery path for the current workspace key. +restart the language-server session or clear its index. Language feature clients route through a provider interface rather than depending directly on an LSP session. Process-free providers remain available @@ -536,23 +361,16 @@ neither a terminal nor a debuggee process exists until an adapter requests one. Debugger disconnect ownership is portable. A session started with `launch` owns its local debuggee and sends `terminateDebuggee: true` when stopping. A -session started with `attach` does not own the remote JVM and sends +session started with `attach` does not own the remote process and sends `terminateDebuggee: false`; closing the native transport must therefore detach without killing the remote process. A session stopped before launch or attach also uses the non-terminating policy. -For JDT LS, the standard initialize handshake and project-import readiness use -separate Core-owned deadlines. Project import fails only after 45 seconds -without changed progress or the 10-minute absolute safety cap; platform clients -must not impose a shorter readiness deadline. The terminal timeout code is -`serviceReadyTimeout`, and its details preserve the last import, download, and -cache snapshot for both products. - ### Explicit debug adapter selection `DebugLaunchConfiguration.adapterProviderID` is optional host routing metadata. -Plugin registrations use the existing Registry identity `pluginID/adapterID`; -the host Java adapter retains `java`. The field is not a DAP argument, a process +Plugin registrations use the existing Registry identity `pluginID/adapterID`. +The field is not a DAP argument, a process command, or a plugin-private function call. An omitted identity uses the existing file selection. An explicit identity, including an empty or withdrawn one, must never fall back to another adapter matching the file. @@ -581,8 +399,8 @@ executable permissions before binding target facts through `debug.adapters.start Build-tree Debug and saved-provider Run Debug save documents first and revalidate workspace, source, configuration, model and plugin identity before handoff. Debug owns the resulting session; stopping preparation or accepting late replies cannot -start a session after invalidation. Java/JDT and cross-plugin preparation are not -covered by this native increment. +start a session after invalidation. Cross-plugin preparation is not covered by +this native increment. ## Error Codes @@ -653,8 +471,8 @@ replace those channels or swallow real source changes mixed with build output. Run configuration behavior is exposed through the `runConfig.*` commands. Platform clients coordinate inspection, generation, resolution, typed document edits, and launch planning, but must not implement a second JSON merger, -toolchain matcher, ID generator, argument parser, or Java/Maven argument -builder. Opening a project inspects existing files without writing; generation +toolchain matcher, ID generator, or argument parser. Ecosystem-specific +arguments are built by the plugin that declared the run. Opening a project inspects existing files without writing; generation is an explicit user action. Shared project overrides stay in `.lithe/run/configurations.json`. Machine-local overrides may live in `.lithe/run/local.json` or in a host-owned document supplied as @@ -669,92 +487,6 @@ than inferred from the provider namespace. An automatically discovered runtime path is session-effective: validation and launch share it, but persistence still requires an explicit user selection. -Maven tool-window execution uses `maven.launchPlan`; platform views do not -assemble Maven arguments. Portable profile and Skip Tests defaults conform to -[`maven-portable-configuration-v1.schema.json`](maven-portable-configuration-v1.schema.json). -The transient Core request conforms to -[`maven-launch-context-v1.schema.json`](maven-launch-context-v1.schema.json). -External `settings.xml`, local repository, Maven executable, and Maven JDK -paths remain in a machine-local store. They may be supplied transiently to Core -for planning and fingerprinting, but Core never opens `settings.xml` or -serializes those paths into the portable project context. - -Expanding a module's Dependencies node starts an on-demand query using -`maven.dependencyPlan`; Core owns the fixed plugin arguments and normalizes the -captured text through `maven.dependencies`. Each platform owns a separate, -bounded Maven process for this query so dependency loading cannot replace build -output or stop an ordinary Maven task. Results are cached by module until the -project or Maven configuration changes. The UI exposes loading, ready, failed, -and cancelled states, rejects stale results, and links every dependency to the -owning module's `pom.xml`. Dependency failure never blocks project loading or -Java editing. - -The Java language-server startup consumes that same context. Core exposes the -selected `settings.xml` to JDT LS as -`java.configuration.maven.userSettings`, then applies the sorted Profile set -to the reactor and every recursively declared Maven module after JDT LS -reports `ServiceReady`. The Java session reaches `ready` at that verified -signal; Profile updates then run as a bounded background task with at most -eight in-flight projects. Each project reports its own result, and a rejected -or timed-out update preserves the usable Java session while exposing a partial -failure that the host can retry. - -Maven-backed framework, test, and module launch planning consumes the current -project Maven context. A Run Configuration's explicit Profiles and toolchain -paths take precedence; explicit `cwd` and `extensions.maven.skipTests` values -also take precedence, including `skipTests: false`. Unset values inherit the -project settings. The shared Core applies the final Maven argument order; -tool-window, framework, test, and module launches add `-am` when reactor -dependencies must be built. - -Project-owned Java Main and a Maven Spring Boot service with a resolved Java -entry source use a different boundary: the Java language service selects the -source target, Java Debug Server builds the owning project and resolves its -runtime classpath/module path, Core projects those structured paths into a -direct JDK launch, and the Run host starts that one JVM. Maven still defines the -project model, but Run never sends either target through a reactor-wide Java -launch goal. A user-authored or legacy Spring Boot service without a resolved Java entry source retains -its Maven-goal compatibility path. Run and Debug share the Java preparation path -so module selection, generated sources, test-source mains, and dependency paths -do not drift. - -Both platforms use `java.prepareLaunch` for source-specific entry selection, -the serialized JDT build, and runtime path resolution. One operation ID owns all -three phases and their cancellation; a cancelled or timed-out attempt cannot -advance on a late upstream reply. Hosts save dirty workspace documents before -preparation and abort on any save failure. JDT remains the authority for project -membership and paths; Core retains only the current attempt's intermediate -results. Native adapters still own JDK discovery and process execution. - -An unsuccessful Java launch build is evidence, not an unconditional host veto. -For `javaBuildCompilationErrors` and `javaBuildFailed`, the language boundary -still resolves the target's runtime paths and returns them with Core's build -report. Run and Debug pause the original attempt and offer Run Anyway, Always -Continue for this workspace, and Cancel; a reported index rebuild recovery also -offers Java: Rebuild Index. Continuing resumes the same attempt and never -issues a second build. Cancellation, timeout, transport failure, and an unknown -build status do not carry a code verdict and remain non-overridable. Build -elapsed time is displayed as evidence only, never used as a trust threshold. -Windows opens the Run tool window when a decision is required, including when -Debug was initiated from the Maven tool window, so no launch waits on an -unmounted prompt. - -Java test actions use the same Maven process lifecycle for a complete JUnit 4 -or JUnit 5 test class and for an individual method. The selector is validated -before launch and is passed through `maven.launchPlan`; no platform assembles a -shell command. Surefire/Failsafe text is normalized through `maven.testResults` -into passed, failed, skipped, and total counts plus ordered failure details. -When a stack frame resolves inside the workspace, the failure links to its -one-based source line. The active test operation owns cancellation and stop; -late output or parsed results cannot replace a newer run. The last valid class -or method selection remains available for an explicit rerun, while cancellation -clears only the active result. - -Maven module menus use Core's resolved `extensions.maven.reactorPath` and -module identity before preferring a default Run configuration. An effective -working-directory override is not project ownership. File-dependent entries -such as Current File are not module launch candidates. - On Windows, Debug cleanup owns a Run execution ID in addition to its reusable output slot. The host checks that ID atomically when stopping the process so a late adapter shutdown cannot terminate a replacement Run in the same slot. @@ -772,25 +504,6 @@ application DOM as executable markup. Rendering and resizable layout are owned by the platform. The behavior fixture is [`svg-preview-v1.json`](../fixtures/editor/svg-preview-v1.json). -### Java project preparation presentation - -Core projects the existing Java session, Maven profile task, and JDT build gate -into `projectPreparation` runtime events. Both products render this in their -status bar and Run panel, with language service settings and diagnostic access. -`starting`, `importing`, `configuring`, and `building` describe actual work; -`ready` describes preparation completion, not successful compilation or a valid -launch configuration. Ordinary indexing is not a run prerequisite. - -Preparation is scoped to the Java workspace session. Only JDT-backed launch -paths honor `blocksRun`; other language and Maven-goal launchers keep their own -prerequisites. Partial profile failures remain visible without blocking unrelated -modules: the target build still validates its own readiness. Consumers reject old -session updates and clear state on explicit stop/workspace replacement. -Core's bounded preparation wait is the single launch gate; platform services and -Run controls do not race it with a second snapshot check. Preparation remains -visible while a click queues behind Core, and a stale visible `ready` state -cannot disagree with a separate host-owned preparation veto. - ### Optional PHP support PHP plugin installation is explicit. The base application ships neither a PHP @@ -846,47 +559,6 @@ text. Register bundled contributions in `frontend/editor`, not in a separate platform-specific tokenizer. Text fixtures are shared under `rust/fixtures/editor/text-content-v1.json`. -### Updating a running Java service - -Run/Debug presentation owns an explicit update action bound to an execution ID, -not the latest Run configuration or focused editor. Platform workflows save the -workspace, reuse the live JDT session to verify the original runtime paths, and -compile with the existing Core build coordinator. Build failures cannot be -bypassed for updates. Core owns Java Debug Server response normalization; hosts -own deadlines, stale-result rejection, UI progress and explicit restart actions. - -Run offers compilation for executions whose launch classpath includes Spring -Boot DevTools. Compilation completion does not establish restart or readiness; -the UI directs users to service output, and respects a project-configured trigger -file. Debug uses HotSwap with DevTools automatic restart disabled at JVM launch. -Remote attach and non-JDT launches have no update action in this first version. -No installed resources or new runtime caches are written: output remains in -JDT-owned workspace build paths and the existing platform-owned JDT state. - -### Agent 响应状态展示 - -- 状态与已观察到的 ACP 进度一致:准备会话、等待响应、收到推理、收到回复、本轮工具执行、等待授权、明确重试和正在停止。静默时长不能证明正在推理或重试,已回放的历史工具不属于当前轮次。 -- 固定 codex-acp 的 `session_info_update._meta.codex.error.willRetry == true` 和非空 `turnId` 是 macOS 重试显示的证据;该通知可能没有 `title`。共享样例 `codexRetry` 由官方 ACP SDK 往返验证,Host 继续透传元数据,不新增请求/事件种类或网络重试策略。 -- 状态隔离到会话,新进度清除重试显示,完成/失败/断连清理本轮状态,正在停止时不因迟到进度回退;不得显示未经脱敏的原始错误或从文本猜测重试次数。请求仍等待上游完成/取消确认,既有十分钟 prompt 上限和十秒取消上限不变。 -- 这次交付 macOS 展示;Windows 对话 UI 的消费与真实验收仍待完成。展示状态只在内存中,不写入 bundle、安装目录或上游历史。 - -### Claude Desktop 机制卡片 - -- Host 识别 Claude Desktop 注入的 MCP 工具(`ccd_*`、`visualize`、`terminal`、`Claude_Browser`、`computer-use`、`scheduled-tasks` 与按 UUID 命名的 claude.ai 连接器),为每个调用给出可读的英文 `title` 和 `kind`;不认识的 MCP 服务原样透传。 -- 有交互含义的调用在 `tool_call` / `tool_call_update` 的 `_meta.lithe.claudeDesktop` 附加卡片,字段 `card` 决定类型:`chapter {title, summary?}`、`nextStep {actions: [{label, message}] ≤4}`、`task {title, summary?, prompt?, state: proposed|withdrawn|started|handedOff}`、`pullRequest {action: bound|unbound|monitored|autoMerge, url, number?, autoFix?, enabled?}`、`directory {action: changed|requested, path}`、`widget {title, code ≤256 KiB 字符}`。文本字段截断到 16 KiB 字符;超限的 `widget.code` 置空。 -- `nextStep` 是 Host 另发的已完成调用,id 为原 `wrap_up` 调用 id 加 `#next`;任务撤回或启动的结果到达后,Host 以原卡片调用 id 补发整张卡片。后到的卡片整体替换先前的卡片;不带卡片的更新不清除卡片。 -- 客户端只在调用状态为 `completed` 时显示卡片;未知 `card` 类型、缺字段或非 http(s) 的 PR 链接退回普通工具行。卡片动作只填写输入框,不代替用户发送。可视化内容必须在禁止网络加载、非持久存储、禁止导航的隔离网页中渲染。 -- 共享样例 `rust/fixtures/agent/claude-desktop-cards-v1.json` 按调用顺序记录 Host 收到的更新(`updates`)和呈现给客户端的结果(`presented`),覆盖六种卡片、只换标题的行和原样透传;Host 测试逐例重放它,客户端按它解码卡片。 - - -Agent skill management groups files by Global/Project and retains their Agent -provenance. Codex-reported capabilities and Claude's partial directory inventory -are distinct; neither file presence nor a management-list row proves that the -current conversation has used a skill. Explicit create/import operations write -only to native user/project directories and preserve existing skills. Explicit toggles write native settings: Codex by discovered path via its official App Server API, Claude by name via skillOverrides in user or project-local settings. Claude states are configured visibility, not effective managed/session policy. Never persist a separate Lithe enabled flag, move packages to simulate disablement, or claim that a change revokes already loaded session content. File -contents remain untrusted document data. Settings discovery and file management -must not create conversations, send prompts, or interrupt existing Agent work. - Build-provider application preparation and execution have separate owners. The workspace Build session validates a current target and prepares it; the native Run host owns the application process, output and stop. Explicit persistence @@ -913,7 +585,6 @@ Automatic Provider run discovery uses the current public declaration/target pair independently of language imports. The host projects generic selectors and upstream labels; Core owns stable Run IDs, layering and snapshot freshness. Neither layer interprets framework IDs or reconstructs commands/toolchains from those selectors. -The existing Java ownership projection remains separate during the remaining migration. Portable Provider user choices live in Run v2 `extensions.build-run`, separately from `extensions.build` selector identity. Hosts preserve exact runtime argument @@ -927,12 +598,3 @@ the prefix does not confer authority. Core deduplicates only the generated count of the effective saved selector and validates it before use. Prepared team/local Run layers can be resolved without filesystem publication; native adapters own cancellation, source checks, metadata-preserving conditional writes, rollback and readback. - -### Project Java runtime and language-server runtime - -Platforms supply the selected project JDK with `javaRuntimes[].isDefault`, reusing -Run's project configuration and requirement-aware selection. The JDT host JVM may -remain the bundled supported JVM. Core projects the selected project home to JDT's -`java.configuration.runtimes[].default`; it must not infer the project VM from the -server executable. An invalid explicit selection must not mark an unrelated discovered candidate as the default; -Windows reports the invalid setting before launch. diff --git a/rust/contracts/ide-api/v1.md b/rust/contracts/ide-api/v1.md index 5166ad8d..4d3de7c1 100644 --- a/rust/contracts/ide-api/v1.md +++ b/rust/contracts/ide-api/v1.md @@ -101,16 +101,10 @@ configuration values, never portable workspace identifiers. | Tool | Arguments | Permission | Result | | --- | --- | --- | --- | | `lithe_project_inspect` | none | read | workspace identity, name, permissions and available readiness state | -| `lithe_environment_inspect` | none | read | project-local `settings`, `discovered`, and `effective` toolchains | -| `lithe_environment_configure` | optional `javaHomePath`, `mavenExecutablePath`, `mavenJavaHomePath`, `settingsPath`, `localRepositoryPath` | configure | persisted/effective settings or explicit failure; omitted settings are preserved | -| `lithe_maven_inspect` | none | read | project/modules, profiles, skip-tests and reload/error state | -| `lithe_maven_configure` | optional `profiles`, `skipTests` | configure | saved profiles and reload state | -| `lithe_maven_reload` | none | execute | outcome of existing project/Java synchronization | | `lithe_build_inspect` | none | read | detected plugin instances, declarations and current model identities | | `lithe_build_reload` | `instanceID` | execute | model reload requested; inspect until model state is ready | | `lithe_build_configure` | `instanceID`, `optionID`, `value` (string, boolean, string list or null) | configure | checked persisted option and reload state; null resets the declared option | | `lithe_build_execute` | `instanceID`, `taskID`, optional `projectID`, `targetID`, `modelTaskID`, `userArguments` | execute | `operationID` for the same Build execution shown by the IDE | -| `lithe_maven_execute` | `goals`, optional `modulePath` | execute | Windows current Maven execution; unavailable on macOS | | `lithe_run_list` | none | read | project configurations with IDs, names, providers and readiness | | `lithe_run_start` | `configurationID` | execute | `operationID` after launch; failure if launch was refused | | `lithe_run_restart` | `operationID` | execute | replacement execution's `operationID` | @@ -118,30 +112,23 @@ configuration values, never portable workspace identifiers. | `lithe_operation_output` | `operationID`, optional opaque `cursor` | read | state, nullable exitCode, bounded output and cursor | | `lithe_operation_stop` | `operationID` | execute | stop request for that exact execution | -Empty configuration paths restore automatic selection. macOS does not serve -`lithe_environment_inspect`, `lithe_environment_configure` or any `lithe_maven_*` -tool: toolchains and build tools belong to plugins there, so an agent reads and -sets their options through `lithe_build_inspect` / `lithe_build_configure` on the -plugin's Build instance, and native XML uses its explicit configuration -workflow. Windows still serves the environment tools, including `settingsPath` -and `localRepositoryPath`, which require a loaded Maven project. An open unsaved -settings draft or an in-progress save returns `BUSY`; the Agent cannot reset -that draft or save unrelated edits. Source read failures remain failures and -never become automatic tool selection. +The catalog has no language- or tool-specific capability. Toolchains and build +tools belong to plugins, so an agent reads and sets their options through +`lithe_build_inspect` / `lithe_build_configure` on the plugin's Build instance, +and a tool-native configuration file uses its explicit configuration workflow. Configuration writes use the existing platform stores; they are not a transaction across multiple settings documents. An error can follow a partial save, so inspect settings before retrying. Credentials, full launch environment values and command arguments are not included in run configuration -listing. Toolchain discovery uses existing installed tools; it does not install -or download a JDK or Maven. Maven execution may download dependencies or execute -plugins, and is therefore an execute capability. +listing. Build execution may download dependencies or execute project code, and +is therefore an execute capability. On macOS, build execution uses the enabled plugin's detected instance and task identities. Read them with `lithe_build_inspect`; model project, target and task selectors require a loaded, current model and are resolved again for each call. A model task also requires its declaring template and project. Arguments remain an array of tokens all the way to the Provider; the host does not reinterpret -Maven goals, construct a command string or use the old Maven execution service. +them or construct a command string. `lithe_build_inspect` also returns each option's ID, kind, effective value, source, choices and allowCustom, plus native-configuration availability. `lithe_build_configure` validates one value with the same current instance/model used by the native option @@ -155,15 +142,12 @@ stale Provider work and invalidate native drafts, models and language import. The same effective Build options, configured tool resolver and workspace Provider serve UI and Agent calls. An active Build execution returns `BUSY`. -macOS exposes the four plugin Build tools and removes all four Maven-specific -tools from discovery and admission. Windows connects Build inspect, reload, configure and -execute to its existing workspace-owned Provider controller, alongside the four -Maven tools whose current consumers are still being migrated. Detection reads +Both products expose the four plugin Build tools. Windows connects Build inspect, +reload, configure and execute to its existing workspace-owned Provider controller. Detection reads only admitted declarations and saved options. Product Build capability admission remains closed; exposing an implemented API does not admit a plugin requiring unimplemented capabilities. Windows configure shares the explicit native UI save path, persists machine-local workspace overrides, checks team/local source revisions and re-detects after publication. Null restores team/declaration inheritance; it does not edit a tool-native configuration file. -An unexposed call returns `NOT_SUPPORTED` before native execution. The complete -Maven project, settings and Run migration remains pending outside these Agent paths. +An unexposed call returns `NOT_SUPPORTED` before native execution. Windows requires a complete, current model before executing a task. Reload uses the existing model preparation and returns immediately; inspect reports its state. @@ -178,7 +162,7 @@ prevents new starts; already accepted work remains owned by the product workspac whose close/plugin lifecycle cancels it and waits for cleanup. Run operates on saved project configurations, not the currently focused editor -file. Existing Java build decisions are still shown in the IDE; the API never +file. Pending build decisions are still shown in the IDE; the API never silently answers them. Output is retained only as long as its existing IDE output slot: replacing a run, removing its tab, or closing the workspace may expire an operation. Callers receive `OPERATION_EXPIRED`, never another run's output. @@ -224,9 +208,9 @@ surface; it does not introduce a new plugin marketplace, sandbox or UI extension - `cargo test --manifest-path rust/Cargo.toml -p lithe-ide-host` - `cargo build --manifest-path rust/Cargo.toml -p lithe-ide-host --features mcp --bin lithe-mcp` - `node --test scripts/ide-mcp/test-ide-mcp.mjs` -- Windows: the host-api, Maven, run and project-environment frontend tests, plus a - native packaged run using the generated client configuration. +- Windows: the host-api and run frontend tests, plus a native packaged run using + the generated client configuration. - macOS: `IdeCapabilitiesTests`, followed by a packaged native run. -- Both platforms: configure JDK/Maven, reload, execute Maven, start a configuration, +- Both platforms: configure a plugin Build option, reload, execute a Build task, start a configuration, read incremental output, restart, reject the old ID, stop, revoke, and close the project. Confirm installed resource manifests/hashes remain unchanged. diff --git a/rust/contracts/maven-launch-context-v1.schema.json b/rust/contracts/maven-launch-context-v1.schema.json deleted file mode 100644 index 74d99573..00000000 --- a/rust/contracts/maven-launch-context-v1.schema.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://lithe.dev/contracts/maven-launch-context-v1.schema.json", - "title": "Lithe Maven Launch Context v1", - "type": "object", - "additionalProperties": false, - "required": ["version", "reactorPath", "profiles", "skipTests"], - "properties": { - "version": { "const": 1 }, - "reactorPath": { "type": "string", "minLength": 1 }, - "profiles": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "settingsPath": { "type": ["string", "null"] }, - "localRepositoryPath": { "type": ["string", "null"] }, - "skipTests": { "type": "boolean" }, - "mavenExecutablePath": { "type": ["string", "null"] }, - "javaHomePath": { "type": ["string", "null"] } - } -} diff --git a/rust/contracts/maven-portable-configuration-v1.schema.json b/rust/contracts/maven-portable-configuration-v1.schema.json deleted file mode 100644 index 13c94584..00000000 --- a/rust/contracts/maven-portable-configuration-v1.schema.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://lithe.dev/contracts/maven-portable-configuration-v1.schema.json", - "title": "Lithe Maven Portable Configuration v1", - "type": "object", - "additionalProperties": false, - "required": ["version", "selectedProfiles", "customProfiles", "skipTests"], - "properties": { - "version": { "const": 1 }, - "selectedProfiles": { - "type": "array", - "items": { "type": "string", "minLength": 1 }, - "uniqueItems": true - }, - "customProfiles": { - "type": "array", - "items": { "type": "string", "minLength": 1 }, - "uniqueItems": true - }, - "skipTests": { "type": "boolean" } - } -} diff --git a/rust/contracts/run-configuration-v1.schema.json b/rust/contracts/run-configuration-v1.schema.json deleted file mode 100644 index 6c09a68d..00000000 --- a/rust/contracts/run-configuration-v1.schema.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://lithe.dev/contracts/run-configuration-v1.schema.json", - "title": "Lithe Run Configuration Document v1", - "type": "object", - "required": ["version", "configurations"], - "properties": { - "version": { "const": 1 }, - "generator": { - "type": "object", - "required": ["fingerprint"], - "properties": { - "fingerprint": { "type": "string", "pattern": "^sha256:" }, - "inputs": { - "type": "object", - "propertyNames": { "type": "string", "minLength": 1 }, - "additionalProperties": { "type": "string", "pattern": "^sha256:" } - } - }, - "additionalProperties": false - }, - "configurations": { - "type": "array", - "items": { "$ref": "#/$defs/configuration" } - } - }, - "$defs": { - "configuration": { - "type": "object", - "required": ["id"], - "properties": { - "id": { "type": "string", "minLength": 1 }, - "name": { "type": "string", "minLength": 1 }, - "type": { "enum": ["java.current-file", "spring-boot.maven", "maven.module"] }, - "module": { "type": "string" }, - "mainClass": { "type": "string" }, - "toolchains": { "type": "object", "additionalProperties": { "type": "string" } }, - "workingDirectory": { "type": "string", "pattern": "^(?!/)(?!.*(^|/)\\.\\.(?:/|$)).*$" }, - "jvmArguments": { "type": "array", "items": { "type": "string" } }, - "programArguments": { "type": "array", "items": { "type": "string" } }, - "mavenProfiles": { "type": "array", "items": { "type": "string" } }, - "mavenSkipTests": { "type": "boolean" }, - "disabled": { "type": "boolean" } - }, - "additionalProperties": false - } - } -} diff --git a/rust/contracts/run-configuration-v2.schema.json b/rust/contracts/run-configuration-v2.schema.json index 057f1e45..e33c6077 100644 --- a/rust/contracts/run-configuration-v2.schema.json +++ b/rust/contracts/run-configuration-v2.schema.json @@ -24,41 +24,11 @@ }, "toolchain": { "type": "object", - "description": "Machine-local project toolchain defaults. Stored only in the local layer; non-empty per-configuration paths override matching defaults.", + "description": "Machine-local project toolchain state. Stored only in the local layer. Core reads and replaces only `preferences`; other fields belong to whichever host or plugin wrote them and are preserved unchanged.", "properties": { - "preferences": { "$ref": "toolchain-consumer-preferences-v1.schema.json" }, - "java": { - "type": "object", - "properties": { - "homePath": { "type": "string" } - }, - "additionalProperties": false - }, - "maven": { - "type": "object", - "properties": { - "executablePath": { "type": "string" }, - "javaHomePath": { "type": "string" } - }, - "additionalProperties": false - }, - "pathOverrides": { - "type": "object", - "description": "Machine-local subproject selections, keyed by normalized workspace-relative root. Empty fields inherit project defaults. Hosts preserve the map when saving individual run configurations.", - "maxProperties": 256, - "propertyNames": { "type": "string", "minLength": 1, "maxLength": 1024, "pattern": "^(?!/)(?!.*//)(?!.*/$)(?!.*(?:^|/)(?:\\.|\\.\\.)(?:/|$))[^\\\\:\\x00-\\x1f\\x7f]+$" }, - "additionalProperties": { - "type": "object", - "additionalProperties": false, - "properties": { - "javaHomePath": { "type": "string", "maxLength": 4096 }, - "mavenExecutablePath": { "type": "string", "maxLength": 4096 }, - "mavenJavaHomePath": { "type": "string", "maxLength": 4096 } - } - } - } + "preferences": { "$ref": "toolchain-consumer-preferences-v1.schema.json" } }, - "additionalProperties": false + "additionalProperties": true }, "configurations": { "type": "array", @@ -235,13 +205,6 @@ "extensions": { "type": "object", "propertyNames": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" }, - "properties": { - "java": { "type": "object", "properties": { - "build": { "$ref": "#/$defs/buildProjectReference" }, - "module": { "type": "string" }, - "mainClass": { "type": ["string", "null"] } - } } - }, "additionalProperties": { "type": "object" } }, "disabled": { "type": "boolean" }, diff --git a/rust/contracts/rust-core-api.md b/rust/contracts/rust-core-api.md index 0d33607b..37636689 100644 --- a/rust/contracts/rust-core-api.md +++ b/rust/contracts/rust-core-api.md @@ -219,7 +219,6 @@ const char *lithe_core_version(void); char *lithe_core_execute_json(const char *request); char *lithe_core_execute_json_with_events(const char *request, void (*callback)(const char *, void *), void *context); int32_t lithe_core_git_askpass(const char *prompt); -char *lithe_core_lsp_provider_catalog_json(const char *workspace_root); int32_t lithe_core_cancel(const char *operation_id); void lithe_core_free_string(char *value); void lithe_core_install_log_waker(void (*callback)(void *), void *context); @@ -229,49 +228,10 @@ int32_t lithe_core_remove_log_waker(void *context); The macOS package uses the small C bridge in `macos/Sources/LitheRustCore/`. The canonical C declarations are in `rust/lithe-core/include/lithe_core.h`. Native clients can link the same `staticlib` or `cdylib`; Rust hosts call -`lithe_core::execute_json` and `lithe_core::cancel_operation` directly. Hosts -call `lithe_core::execution::plan_launch_command` (or the -`execution.planLaunchCommand` JSON command) before spawning a Java process. The -request fields are `executable` (required, non-empty launcher path), `arguments` -(string array, excluding the executable), `javaFeatureVersion` (optional -integer), `javaReleaseContents` (optional string array of host-read `release` -file texts, nearest first), and `commandLineLimit` (optional byte budget; default -30719, which is 32767 - 2048). An explicit `javaFeatureVersion` takes precedence -over the contents. Core reads the first entry containing `JAVA_VERSION=`; an -unrecognized value is unknown and does not fall back to a farther entry. The -planner changes a launch only when the executable file name is `java`, `java.exe`, -`javaw` or `javaw.exe`, the estimated command line exceeds the limit, the JDK -feature version is known and at least 9, and a path-list option (`-cp`, -`-classpath`, `--class-path`, `-p` or `--module-path`, in separate or `=` form) -precedes the application target. Planning stops at the application target (main -class, `-jar`, or `-m`/`--module`); program arguments stay verbatim. The response -`data` is `{ arguments, argumentFile }`. `arguments` is the request list unchanged -when no file is needed. Otherwise the element at `argumentFile.argumentIndex` is an -empty placeholder that the host replaces with `@`, and -`argumentFile` is `{ text, argumentIndex }`; it is `null` when the launch is -unchanged. `lithe_core::execution::java_feature_version_from_release` reads the -feature version from one release file text. -Core owns the Unicode argument-file text and quoting. The Windows host encodes -that text losslessly using the launcher's actual system code page, independently -of the JDK feature version: JEP 400 does not make native launcher arguments UTF-8. -An unrepresentable path is reported as an actionable failure, never substituted. -The host also escapes backslash bytes introduced by multibyte encoding inside -quoted values, since the native argument-file parser processes bytes. -Every execution owns an exclusively created temporary file; partial writes and -spawn failures clean it up, while successful launches retain it until that exact -process exits. A replacement execution never shares its predecessor's file. -The `plan_classpath_jar_launch` fallback for known JDK versions older than 9 is not -yet provided: there is no Rust API or JSON command for it. Those launches stay -direct, so an oversized Windows JDK 8 command line is not shortened yet. The macOS -process argument limit (`ARG_MAX`) is far above the Windows cap, so a direct JDK 8 -launch already succeeds there. +`lithe_core::execute_json` and `lithe_core::cancel_operation` directly. Strings returned by the core are UTF-8 JSON allocated by Rust. The caller must release response strings with `lithe_core_free_string`. -Rust native adapters may call `execution::java_feature_version_from_release` to read the -JDK feature version from bounded host-read `release` metadata, then reuse Core's -runtime candidate ordering. Passive Build discovery never falls back to running -Java when that metadata is missing or invalid; explicit tool execution owns probes. ### Localized action search `search.actions` accepts `{query, candidates: [{texts: [string]}]}` and returns @@ -380,79 +340,6 @@ subsystem: `mcp.parseConfiguration`, `mcp.parseRegistry`, `mcp.featuredRegistry` `rust/fixtures/mcp` and `rust/contracts/mcp-registry-page.json`. The IDE-facing MCP server (`lithe-mcp` plus `rust/fixtures/ide-api/mcp-protocol.json`) is unaffected and stays in `lithe-ide-host`. -### Maven test results - -`maven.testResults` accepts `{ "root": string, "output": string }` and parses -the bounded text emitted by Maven Surefire or Failsafe after a JUnit 4/5 class -or method run. `root` must be an existing workspace directory and `output` is -limited to 500,000 characters. The response is: - -```json -{ - "testsRun": 4, - "failures": 1, - "errors": 1, - "skipped": 1, - "passed": 1, - "success": false, - "failureDetails": [ - { - "name": "additionIsCorrect(com.example.CalculatorTest)", - "kind": "failure", - "message": "expected <4> but was <5>", - "path": "src/test/java/com/example/CalculatorTest.java", - "line": 42, - "column": null - } - ] -} -``` - -`kind` is `failure` or `error`; `path` is a workspace-relative source path -when a stack frame or failure footer can be resolved unambiguously. Relative -source lookup requires a complete workspace index, prefers a unique full -package-path suffix at a path boundary, and falls back to a unique filename -only when no package-path candidate exists. Ambiguous matches or an incomplete -scan (including the 10,000-directory limit) leave the location `null`. -All locations use one-based lines with nullable columns. `passed` is derived from the summary and never -negative. A final `Results` summary is preferred; when Maven only prints -per-class summaries, the counts are aggregated. Failure details retain Maven's output order and are bounded to -10,000 entries. A parser or size violation returns the standard -`parse_failed` error. - -The request may also carry `reports: { module, sourcePath, classes, -notBeforeMillis }` naming the run's workspace-relative Maven module directory -(`null` or `.` for the root), the binary names of the selected test classes, and -the run's start time in Unix milliseconds. When `module` is absent and -`sourcePath` names a workspace-relative test file, the module is the nearest -directory above that file holding a `pom.xml`. An empty or absent `classes` -reads every report in the module written at or after `notBeforeMillis`. Core then reads the Surefire/Failsafe XML reports -the run wrote and returns `testCases`, otherwise an empty list: - -```json -{ "className": "com.example.CalculatorTest", "method": "additionIsCorrect", - "status": "failed", "message": "expected <4> but was <5>", "invocations": 1, - "durationMillis": 23 } -``` - -Optional `durationMillis` comes only from each XML testcase's `time` seconds attribute. Repeated/parameterized invocations sum their times in whole milliseconds; if any invocation has a missing, negative, non-finite, unrepresentable time or the sum overflows the host integer range, the field is omitted. A reported zero is valid. Hosts hide missing method durations and must not substitute a console/suite summary or another class's method time. `build.decodeTestResults` preserves the same duration in its canonical testcase result. - -Reports are searched in the module's configured Surefire/Failsafe -`reportsDirectory` values plus `/surefire-reports` and -`failsafe-reports`. Only `TEST-.xml` and nested `TEST-$*.xml` -files modified at or after `notBeforeMillis` are read, so an earlier run's -reports never produce outcomes. `method` drops parameter lists and invocation -indexes; invocations that report a display name instead of a method are -omitted. `status` is `passed`, `failed`, `error`, or `skipped`; when a method -has several invocations, the most severe wins (error, failed, passed, skipped) -and `message` comes from the first invocation with that status. Results are -ordered by `className`, then `method`. Core reads at most 512 reports of up to -32 MiB each and returns at most 10,000 methods; malformed reports are skipped. -A module outside the workspace or a class name that is not a Java binary name -returns `invalid_request`. A class with no case means "no recorded outcome", -never "passed". Platform stores must associate the response with the -launch operation and discard it after cancellation, replacement, or workspace -change. Workspace paths in responses are relative and use `/` separators. Line numbers are one-based. `git.status.repositoryRoot` may be an absolute path when the @@ -534,7 +421,7 @@ the source as `originalPath`; platform mutations that act on the Git entry pass both paths back to Core. The core rejects absolute paths and `..` traversal for file commands. Native file dialogs, file watching, PTY/ConPTY, -Java processes, and runtime discovery remain platform adapters. +and runtime discovery remain platform adapters. `git.history.recentReferences` contains at most five existing local branches in most-recently-used order. The current branch is first. Core derives checkout @@ -1319,7 +1206,8 @@ a filter (omit it to show every commit), `displayMode` is `compact` (default) or `expanded`, and `currentBranch` asks for the ancestry of that local branch. Hosts call it once per loaded window, never per commit, and recompute the whole window when a page is appended or the history refreshes; lanes depend on every loaded edge, and a head -fragment's color ID is the Java `String.hashCode` of its principal reference name, so a +fragment's color ID is the wrapping 32-bit hash `h = 31 * h + unit` over the UTF-16 +units of its principal reference name (the value IDEA's `GraphColorManagerImpl` uses), so a named branch keeps its color across pages. The response is `{ "version", "rows", "laneCount", "recommendedLaneCount", "hasMissingParents", "currentBranchHashes" }`. Each visible row carries `commitHash`, integer `lane` and `laneCount`, a signed @@ -1714,9 +1602,8 @@ visibility. Names share index invalidation and reported file updates; changes inside unwatched generated directories require workspace/index refresh. No index files or resources are written to disk. macOS Search Everywhere requests pages of 50 matches per source; legacy synchronous consumers retain their requested caps. -Java type and method results still use the existing syntax index and include a -one-based line, `symbolName`, and source line in `preview`; language-provider -workspace-symbol integration and external-library search are not implied. +Name search does not read language syntax: language-provider workspace-symbol +integration and external-library search are not implied. The shared name/range examples are in `rust/fixtures/search/name-matching.json`. `workspace.replacePreview` accepts `root`, `query`, `replacement`, the same @@ -1903,7 +1790,7 @@ The `debug.*` commands are the shared Debug Adapter Protocol boundary. Rust owns DAP framing, request sequences, response correlation, initialization and execution state, deterministic breakpoint sets, and normalized thread, stack, scope, variable, evaluation, output, stop, continue, and termination events. -Platforms own adapter discovery, JDT LS activation, sockets or process pipes, +Platforms own adapter discovery, sockets or process pipes, native process termination, persistence, and UI rendering. `debug.validateAdapterEndpoint` accepts exactly `{ host, port }`. `host` is a @@ -2227,10 +2114,10 @@ native clients must not treat its `source.path` as a workspace file. Exception information is available only while paused and normalizes the exception type, description, break mode, optional -stack trace, evaluation name, and nested exception details. The Java adapter -currently supplies the type, description, and break mode but no expandable -exception object reference, so native clients continue to inspect ordinary -frame scopes for local state. +stack trace, evaluation name, and nested exception details. An adapter that +supplies the type, description, and break mode without an expandable exception +object reference leaves native clients to inspect ordinary frame scopes for +local state. Terminal operation events are exactly one of `operationCompleted` with a typed result or `operationFailed` with the adapter command and safe message. Other ordered events are `stateChanged`, `initialized`, `output`, `stopped`, @@ -2253,7 +2140,7 @@ absent, Core derives DAP `terminateDebuggee` from the session's request kind: use `false`. A provided value applies to `launch` sessions only, so a user can keep a local debuggee running. An `attach` session never terminates the external process, even when `terminateDebuggee: true` is sent. This prevents a remote -detach from killing a JVM the IDE does not own. +detach from killing a process the IDE does not own. The compatibility cases are in `rust/fixtures/debug/disconnect-policy-v1.json`. The platform keeps the socket or process alive long enough to flush the frame, then closes it and @@ -2310,21 +2197,14 @@ the Project view's natural file-name order. Severity is the LSP number; missing or out-of-range values are unknown (`0`) and rank last. Cases: `rust/fixtures/lsp/problems-query-v1.json`. -The LSP provider catalog is returned by `lithe_core_lsp_provider_catalog_json`. -Each provider descriptor may include `languageServerLaunch` with ordered -`executableNames` and `arguments`; Swift adapters use this metadata when they -need to discover a real language-server executable; the selected launch plan is -then submitted to the Rust-owned runtime. Built-in descriptors are merged by provider ID with the optional -`.lithe/lsp/language-providers.json` workspace document. See -[`.agents/notes/implemented/architecture/2026-09-13-language-tooling-and-lsp-runtime-ownership.md`](https://github.com/1lck/Lithe-IDEA/blob/preview/.agents/notes/implemented/architecture/2026-09-13-language-tooling-and-lsp-runtime-ownership.md) -for routing, discovery, lifecycle, and compatibility rules. +Core ships no built-in language-provider catalog. Every language server comes +from a plugin's `languageServers` declaration, which Core validates and plans. The `lsp.*Server`, `lsp.*Document`, `lsp.request`, `lsp.pollEvents`, and `lsp.waitEvents` commands are the semantic LSP runtime boundary. `lsp.startServer` accepts the provider ID, selected executable/arguments/environment, root URI, working -directory, initialization options, optional runtime executable, -`jdtlsLaunchResources`, cache directory, `workspaceFingerprint`, and the +directory, initialization options, and the optional `pluginId` of the plugin that owns the process, its optional `workspaceConfiguration` (an object, plugin-owned sessions only), its optional `hostServices` (the launch plan's `backend.hostServices`; @@ -2333,117 +2213,8 @@ its optional `hostOptions` (plugin-owned sessions only; `editor:{actionIcons, actionSourceBytes, providerTypes}` declares the icons, collector source bound and provider types the host implements, reported through `lithe/host/getCapabilities` to sessions granted the matching editor methods), plus -initialize, post-initialize readiness, request, Java project build -(`javaBuildTimeoutMilliseconds`), and shutdown deadlines. -Java callers may also provide the versioned `mavenContext` accepted by -`maven.launchPlan`. Core validates its reactor and recursively declared modules, -publishes the user-level settings through -`java.configuration.maven.userSettings` and the selected installation's -`conf/settings.xml` through `java.configuration.maven.globalSettings`, and, -after `ServiceReady`, sends one -`java.project.updateSettings` command per Maven project with -`org.eclipse.m2e.core.selectedProfiles`. Maven Java, test, and generated source -roots are normalized to workspace-relative `java.project.sourcePaths` during -the same configuration flow, so JDT LS receives the selected reactor's source -model without platform-specific POM parsing. Both settings documents are passed as content-addressed copies inside the -session's JDT LS state directory (`/.lithe/maven/`). The user-level copy -comes from `settingsPath`, else Maven's default `~/.m2/settings.xml`, else an -empty document when only `localRepositoryPath` is set, and carries that local -repository override. JDT LS detects settings changes by comparing paths, so a -content change must always produce a new path. An unreadable document is passed -by its original path with a session warning instead of failing startup. -Copies remain readable until JDT workspace cache eviction or index rebuilding; -notification delivery is not an acknowledgement that the server read them. - -Gradle callers may also provide `javaGradle = { wrapperPresent, offline?, userHome? }`, where -`wrapperPresent` reports whether the root ships -`gradle/wrapper/gradle-wrapper.properties`. When present, Core states -`java.import.gradle.enabled` in `settings.java` and, if that descriptor exists, -`java.import.gradle.wrapper.enabled`. Both leaves equal JDT LS's own defaults, -so the section declares the import behavior Lithe relies on instead of changing -it; Buildship itself decides which distribution to use, honoring the wrapper only -when the project ships that descriptor. Settings that require reading the build -script (`java.home`, `version`) are never guessed. `offline` and `userHome` -are the user's own build options, as `build.languageImport` maps them from the -plugin that declares the Gradle build system; Core sends -`java.import.gradle.offline.enabled` and `java.import.gradle.user.home` only -when they are set, and the session keeps them until it restarts. A workspace -whose root declares no Gradle build file sends no `javaGradle` and therefore -receives no `java.import` settings. - -`lsp.updateMavenConfiguration` accepts `{ sessionId, mavenContext, -reloadProjects? }` for a running Java session started with a `mavenContext`, -and returns `{ settingsChanged, projectsReloaded, profilesUpdating }`. When the -settings copies differ from the ones JDT LS holds, Core sends -`workspace/didChangeConfiguration` and JDT LS force-updates every Maven project -itself. When they are unchanged and `reloadProjects` is `true`, Core sends -`java/projectConfigurationsUpdate` for the reactor's project URIs, which -re-resolves dependencies even though no `pom.xml` changed. Changed profiles -restart the profile task once the session is ready. Before the `initialized` -handshake, the new configuration replaces the one the pending settings -notification sends. Explicit reloads received before `ServiceReady` are coalesced -and sent once projects are ready, even when settings are unchanged. Response -booleans describe actions sent immediately, not queued work. A newer profile -selection is applied after the preceding batch terminates, including failed -batches; timed-out requests must all drain before that follow-up can start. -The same failed selection is not automatically retried. -A stopped or failed session returns `invalidRequest`. -Resolution problems are not part of the response; JDT LS reports them as -`pom.xml` diagnostics. - -Maven profile application is a -bounded background task: at most eight project commands are in flight, remaining -projects are queued, and each project reports `running`, `succeeded`, or -`failed` with optional error details. Project results use a redacted stable -`projectUri` identifier; they never expose the user's absolute workspace path. -The runtime event also carries the aggregate Maven task status (`running`, -`succeeded`, `partiallySucceeded`, `failed`, `timedOut`, or `cancelled`) so hosts -do not need to infer task completion from log text. A project failure or task timeout does not -terminate an otherwise usable JDT LS session; the host receives a partial-failure -event and may retry. The session reaches `ready` after JDT LS `ServiceReady`. -Core only accepts retries for a ready Java session. A timeout sends `$/cancelRequest` -but retains each in-flight slot until its terminal response arrives. Retry is -rejected while the previous batch is still stopping; if JDT LS never responds, -the user must restart the Java session. Late responses release those slots -without changing the timed-out results. -Hosts reset project results on the structured `mavenProfileTask: "running"` -event and consume `mavenProfileProject` updates directly, scoped to the current -session. Java import completion and Maven task completion use separate UI -notifications so service readiness cannot overwrite a Maven failure. -The session continues to expose profile progress independently. `initializeTimeoutMilliseconds` -only bounds the standard LSP handshake. For a provider such as JDT LS that has -a later readiness signal, -the profile task records a deterministic digest of Maven settings, selected -profiles, project URIs, and source paths; an unchanged successful digest skips -reapplying the same settings, while an explicit retry invalidates that digest. -Hosts may consume lifecycle events for the shared `serverConnected`, -`projectImporting`, `profileApplying`, and `fullyReady` phases. -`serviceReadyIdleTimeoutMilliseconds` retains its wire name but now sets the -quiet-progress **warning** threshold (45 seconds by default). A quiet interval -emits one warning with the last progress snapshot; changed progress rearms the -warning, while duplicate progress does not. JDT progress is not a heartbeat: -silence must not fail the session or terminate its process. -`serviceReadyAbsoluteTimeoutMilliseconds` remains the sole post-initialize -readiness deadline (10 minutes by default), regardless of progress activity. `jdtlsLaunchResources`, when present, -contains `launcherJarPath`, `configurationDirectory`, `lombokAgentPath`, the -legacy optional `javaDebugBundlePath`, and ordered -`javaExtensionBundlePaths`. It is valid only for the Java provider and requires -`runtimeExecutablePath`. Rust loads the legacy Debug bundle first when present, -then appends the extension bundle paths with stable de-duplication. Rust -then uses `runtimeExecutablePath` as the process executable and constructs the -complete deterministic JDT LS JVM argument list. `configurationDirectory` names -the packaged, read-only configuration; Rust never passes it to Equinox, which -writes framework state into its `-configuration` directory. Rust copies the -directory's `config.ini` into -`cacheDirectory/jdtls-configuration//configuration`, -rewrites a missing or damaged copy, and passes that directory instead. A -`cacheDirectory` that resolves inside the JDT LS installation, including -through a symbolic link, fails with `invalid_request` before anything is written; -a missing `config.ini` fails with `process_start_failed`. Areas of other digests -unused for the JDT cache retention period are removed after the area is -prepared, and a removal failure is logged without failing the start. When the structured object is -absent, the selected `executablePath` and legacy wrapper arguments remain the -compatibility path. Rust owns the returned +initialize, request, and shutdown deadlines. +Rust owns the returned session's child process, stdin/stdout/stderr, framing buffer, JSON-RPC request IDs, document versions, pending deadlines, capabilities, diagnostics, and graceful/forced termination. @@ -2456,115 +2227,6 @@ remains incomplete. The original startup error is preserved. Native owners using the in-process registration callback retain their registered identity even when startup returns an error and retry `lsp.destroyServer` themselves. -JDT LS remains `initializing` until `language/status: ServiceReady`. During this -phase Rust reduces changed `$/progress` notifications into throttled JSON log -details containing the current phase, percentage, project, observed project -count, artifact name, repository host, downloaded/total bytes, calculated -throughput, elapsed/idle durations, and cache disposition. Progress parsing is -observability-only and never substitutes for `ServiceReady`. Absolute readiness failures use `serviceReadyTimeout` at stage `serviceReady` -and retain the final diagnostic snapshot in `underlyingMessage` with -`timeoutKind: "absolute"`. Classification still describes observed silence or -transfer activity; it does not prove a JVM deadlock. Quiet warnings preserve the -initializing/preparation state. Explicit server errors and exits still fail -immediately. Shared timing examples live in -`rust/fixtures/lsp/jdt-readiness-v1.json`. - -Platform adapters own filesystem discovery and validate that packaged JDT LS -contains the Equinox launcher, platform configuration directory, Lombok agent, -Java Debug Server, and bundled Java. Java Test-capable hosts additionally -validate their extension bundles and runner. They do not construct JVM commands. -Packaged macOS and Windows plans always use structured direct launch, so runtime -startup has no shell, PowerShell, or user-`PATH` dependency. Wrapper launch -remains optional only for external or older plans. - -For JDT LS, platform adapters observe which root Maven/Gradle descriptors -exist, names of direct Maven module directories, the Gradle wrapper's -`distributionUrl` when the workspace has one, and the selected JDT LS version. They submit those raw observations to `java.jdtWorkspaceFingerprint`; -Rust Core validates, sorts, de-duplicates, and constructs the sole portable -fingerprint representation. Core then hashes the normalized workspace identity -followed by a null separator and that opaque fingerprint to select -`cacheDirectory/jdtls/`. -Omitting the fingerprint preserves the legacy path-only key for older clients. -Changing structure selects a new directory without deleting the old one, so a -later switch back can reuse it. Editing a descriptor is not a structural change: -JDT LS compares each POM and Gradle file's timestamp with the state it saved and -updates only the changed projects on restart, and running sessions receive the -edit through `lsp.workspaceFilesChanged`. Keying the directory on descriptor -timestamps would discard the whole index after any touch of the file. The Gradle -version is the exception: JDT LS does not check the wrapper descriptor, so the -wrapper's `distributionUrl` takes part in the fingerprint and a version change -selects a fresh directory. - -Core starts JDT LS with -`-Djava.import.generatesMetadataFilesAtProjectRoot=false`, so the Eclipse -project files JDT LS maintains (`.project`, `.classpath`, `.factorypath`, and -`.settings/*.prefs`) live in that state directory instead of the user's -modules. Before launch, Core removes such files that earlier versions left in a -Maven or Gradle module directory when the enclosing Git repository does not -track them; tracked files and workspaces outside Git are left alone because JDT -LS keeps honoring files that already exist at a module root. Tracking is decided -by the repository that owns each module, located through its `.git` entry, with -one batched `git ls-files` query per repository; modules outside Git start no Git -process. When any file is removed, Core also deletes the current state directory -so the launch imports the modules afresh instead of reusing a model that points -at the removed files, and records the removed workspace-relative paths in the -session's `info` log event. - -`java.jdtWorkspaceFingerprint` accepts -`{ buildFiles, directMavenModules, gradleWrapperDistribution?, jdtlsVersion }`. -Each build-file observation contains only a workspace-relative `path`; -`gradleWrapperDistribution`, when sent, must not be blank. It returns `{ workspaceFingerprint }`; platforms must not recreate -or parse this opaque string. Compatibility cases are in -`rust/fixtures/lsp/jdt-workspace-fingerprint-v1.json`. - -`lsp.jdtWorkspaceKey` accepts `{ workspaceRoot, workspaceFingerprint? }` and -returns `{ workspaceKey }` through the same normalization and SHA-256 algorithm -used by `lsp.startServer`. Platform cache-maintenance actions use it to remove -only the current workspace/fingerprint directory; they do not clear sibling -workspaces or older structural states. - -`java.runMarkers` accepts `{ mainMethods, testItems, testCases }`: the -`methods` of one file's `javaMainMethods` answer, the `items` of its -`javaTestItems` answer (empty when the platform cannot run tests for the file), -and canonical build-provider report cases (`suite`, `case`, `status`). The older -`className`/`method` field names remain accepted for Windows compatibility. -It returns `{ markers }` ordered by -zero-based `line`, then `kind` (`main`, `testClass`, `testMethod`). `endLine` -is the last line of the declaration as reported upstream: the end of the body for -test classes and methods, and the name line for `main`. Each marker -has a `label` for menus (`App.main()`, `OrderTest`, `OrderTest.creates`), the -launch target (`mainClass`/`projectName`, or `testClass`/`testMethod` and the -Java Test `testItemId`), and `status`: `none`, `passed`, `failed` (failure or -error), or `skipped`. Test cases match a method by class and method name, with -nested-class `$` and `.` treated alike. A class is `failed` when any recorded -method under it (including nested classes) failed and `passed` when at least -one passed and none failed. Items without a source range, such as inherited -test methods, get no marker. The command is pure and never reads the file -system. `rust/fixtures/java/run-markers-v1.json` is the compatibility fixture. - -`java.workspacePolicy` accepts `workspacePaths` and `changedPaths` as -workspace-relative paths. It starts Java tooling when a non-ignored `.java` -source exists and the workspace shows evidence of being a Java project: a build -descriptor (`pom.xml`, `build.gradle[.kts]`, `settings.gradle[.kts]`, a Maven or -Gradle wrapper) no more than two directories below the root, or a `.java` source -no more than two directories below the root for projects that have no build -system. A Java sample or fixture checked into a repository of another ecosystem -therefore does not activate Java tooling; hosts still start a language server on -demand when the user opens a `.java` file. The command chooses one deterministic -representative source and classifies changes as `ignored`, `source`, -`buildConfiguration`, or `other`. The compatibility examples are in -`rust/fixtures/lsp/java-workspace-policy-v1.json`. - -`java.jdtCacheRetention` accepts platform-observed cache directory metadata as -`{ nowUnixSeconds, activeWorkspaceKey?, entries }`. Each entry contains a -lowercase 64-character SHA-256 `workspaceKey` and -`lastModifiedUnixSeconds`. Core ignores invalid candidate names, de-duplicates -observations using the newest timestamp, never selects the active key, and -returns deterministically sorted `expiredWorkspaceKeys` older than the fixed -30-day retention period. Platform adapters own the last-used marker, directory -enumeration, revalidation, deletion, and error logging; Core performs no cache -filesystem I/O. See `rust/fixtures/lsp/jdt-cache-retention-v1.json`. - `lsp.documentIdentity` accepts `{ workspaceRoot, input, platform }` with the same `posix | windows | auto` path syntax. It returns `{ document: { path, relativePath } | null }`. `input` may be a file URI, native absolute path, or workspace-relative path. @@ -2586,8 +2248,8 @@ separator, and percent-encodes reserved characters and UTF-8. Invalid paths return `invalid_request`. See `rust/fixtures/lsp/document-uri-v1.json`. The `rootUri` of `lsp.startServer` and the `uri` fields of `lsp.syncDocument`, -`lsp.saveDocument`, `lsp.closeDocument`, `lsp.request`, `java.navigationMarkers`, -`java.resolveNavigation`, and each `lsp.workspaceFilesChanged` change accept +`lsp.saveDocument`, `lsp.closeDocument`, `lsp.request`, and each +`lsp.workspaceFilesChanged` change accept either an existing URI string or `{ path, platform }`. The related diagnostic location URIs in `lsp.request` accept the same shape. Core converts path inputs inside the existing request; virtual URI strings and opaque server data are not @@ -2625,8 +2287,8 @@ operation. The host flushes current editor text before the save, including edits made while the native write was in flight. Before launch, hosts retry save notifications for clean open buffers and propagate transport failures instead of treating disk persistence alone as launch readiness. -This is distinct from `didChange`: JDT reloads its on-disk working copy on save, -which makes a subsequent Java launch build observe saved dependency edits. +This is distinct from `didChange`: a server that keeps on-disk working copies +reloads them on save, so a later build observes the saved edits. Shared cases: `rust/fixtures/lsp/document-save-v1.json`. `lsp.workspaceFilesChanged` accepts a session ID and ordered file URI changes @@ -2635,24 +2297,6 @@ whose `kind` is `created`, `changed`, or `deleted`. It emits one versioned `lsp.syncDocument`; adapters must not duplicate those edits as watcher events. -`java.navigationMarkers` accepts -`{ sessionId, operationId?, uri, documentVersion? }` and completes with -`{ documentVersion, markers }`. Rust combines JDT LS implementation CodeLens, -`textDocument/implementation`, and `java/findLinks` results with parser-selected -Java declaration candidates. Work is capped at 64 semantic tasks, individual -task failures preserve other verified markers, stale document versions cancel -the batch, and the latest completed version is cached per URI. Markers are -sorted by line, UTF-16 column, and relation. A marker contains `direction` -(`up` or `down`) and `relation` (`interface` or `inheritance`); zero-target -declarations are omitted. The cross-platform examples are in -`rust/fixtures/lsp/java-navigation-v1.json`. - -`java.resolveNavigation` accepts the marker position, direction, relation, and -document version. Downward markers use `textDocument/implementation`; upward -markers use JDT LS `java/findLinks` with `superImplementation`. Its terminal -result is `{ documentVersion, locations }`, using the same normalized physical -and virtual-location representation as ordinary navigation. - `lsp.buildRequest` accepts `{ sessionId, operationId?, method, params, timeoutMilliseconds? }` and returns `{ operationId }`. `method` and `params` are the `provider` field of a `build.modelPlan`, `build.dependencyPlan` or @@ -2879,7 +2523,7 @@ implementation, rename, formatting, range formatting, document symbols, code act inlay hints, full-document semantic tokens, folding ranges, document links (`documentLinks`, result `{ links: [{ range, target, tooltip }] }`; `target` is null until a server supplies it, and entries without a range are dropped), workspace -symbols (`workspaceSymbols`, see below), code lens, provider virtual documents, `javaEntrypoints`, `javaTestItems`, and `javaMainMethods`. +symbols (`workspaceSymbols`, see below), code lens, and provider virtual documents. For code actions, `diagnostics` contains the original `LspClientDiagnostic` objects returned by Core: nullable integer `severity`, `tags`, and `relatedInformation` remain intact, and every range uses zero-based `line` / @@ -2887,102 +2531,11 @@ objects returned by Core: nullable integer `severity`, `tags`, and not a diagnostic request model. Non-LSP markers may request actions with a range and an empty diagnostic context. The round-trip fixture is `rust/fixtures/lsp/diagnostic-code-actions-v1.json`. -`javaEntrypoints` invokes Java Debug Server's -`vscode.java.resolveMainClass` for the session workspace and returns schema -version 1 with deterministic workspace-relative `{ sourcePath, mainClass, -projectName? }` entries plus diagnostics for unusable upstream records. -Discovery queries all entries in the owned JDT session, then filters source -paths against the opened workspace and its resolved location. This includes -plain folders represented by invisible projects in JDT's writable cache; -unrelated workspace sources remain excluded. Discovery -waits behind pending Maven profile updates and JDT project-configuration jobs, -using the same configuration barrier as the following Java build. Up to 128 -waiting discovery operations are allowed per session. Their deadlines include -configuration waiting and the eventual upstream query, bounded by the session's -Java build timeout. Cancellation before dispatch completes that operation without -writing a JSON-RPC discovery request or cancellation; shutdown/failure settles -every waiter. A completed profile update must precede discovery of source roots -introduced by that profile. - -`javaTestItems` requires a file URI, invokes Java Test's -`vscode.java.test.findTestTypesAndMethods`, and returns schema version 1 with a -typed class/method tree. Each item carries the upstream identity, label, fully -qualified name, project, test kind/level, optional JDT handler and sort text, -optional zero-based UTF-16 range, and children. A `null` upstream result means -the file has no tests (Java Test leaves its root's children unset) and yields an -empty item list; any other non-list top-level result is an -`invalidServerResult`, never an empty semantic answer. -`javaMainMethods` requires a file URI, invokes Java Debug Server's -`vscode.java.resolveMainMethod`, and returns schema version 1 with -`{ methods: [{ range, mainClass, projectName? }], diagnostics }`. `range` is the -zero-based UTF-16 range of the method name, and methods are ordered by source -position. `mainClass` matches the `javaEntrypoints` entry generated for the same -file, so editors can launch the workspace configuration for that entry. A `null` -upstream result means no launchable method; any other non-list result is an -`invalidServerResult`. Core does not infer Java entry points or tests from -source syntax. -For the Java provider, an `executeCommand` whose command is -`vscode.java.buildWorkspace` is coordinated by Core instead of being written -immediately. Core writes it only when no earlier build is awaiting its JDT -response, the Maven profile task is not `running`, and no JDT work-done -progress job named `Update project …`, `Updating project configurations`, -`Applying the selected build files…`, or `Updating workspace folders` is open; -a job without progress for 120 seconds stops blocking. Identical queued build -commands share one JDT request, while a running build is never shared. Each -caller is bounded by `javaBuildTimeoutMilliseconds` from `lsp.startServer` -(default 600000), measured from submission; its `requestTimeout` error has stage -`javaBuild` and an `underlyingMessage` naming the reached phase -(`building`, `waitingForPreviousBuild`, `waitingForMavenProfiles`, or -`waitingForProjectConfiguration`). Cancellation or timeout sends -`$/cancelRequest` only when no running or queued caller still needs a build, -and the build keeps its slot until JDT answers. A successful build completes -with the unchanged `{ value: 1 }` result. Other JDT `BuildWorkspaceStatus` -values complete with stage `javaBuild` and code `javaBuildCompilationErrors` -(`WITH_ERROR`), `javaBuildFailed` (`FAILED`), `javaBuildCancelled` -(`CANCELLED`), or `invalidServerResult`. `javaBuildCompilationErrors` and -`javaBuildFailed` additionally carry `javaBuildReport` with `markerScope` -(`launchTarget` or `workspace`), `builderFailedEarlier`, -`elapsedMilliseconds`, and `recovery` (`none` or `rebuildJavaIndex`). The marker -scope is inferred from the dispatched command: `launchTarget` means the request -named a project, not that Core observed Java Debug Server's final project -selection. Elapsed time is evidence only and must not become a heuristic gate. -The versioned examples are in -`rust/fixtures/lsp/java-build-report-v1.json`. - -Hosts treat these two terminal build outcomes as evidence rather than an -irrevocable launch veto. After resolving the usable runtime paths, they may let -the user continue that same launch attempt without issuing another build. A -cancelled, timed-out, rejected, or unrecognized build has no usable verdict and -must be retried instead of overridden. Hosts present Core's message and report -instead of inferring a cause. Core logs `Java project build is waiting` (with `reason`), -`Java project build started`, and `Java project build finished` (with -`outcome`, `errorCode`, `elapsedMilliseconds`, and `waiterCount`). -Background build retries and deadline cancellations are written by a separate -session-owned worker. The deadline monitor never writes to stdin. A background -write that exceeds `requestTimeoutMilliseconds` fails the session with -`transportFailed` at stage `outboundMaintenance` and terminates the server to -release the stalled pipe; the Java build deadline still bounds queue and build time. -`java.prepareLaunch` accepts `{ sessionId, operationId?, sourcePath, mainClass? }`. -`sourcePath` is the absolute saved source path at the native boundary. -Core owns one operation across JDT entry-point discovery, the existing serialized -build coordinator, and classpath resolution. The source must be inside the session -workspace; an optional configured class disambiguates multiple entries in that -exact file (including JDT's module prefix). Core does not infer entry points or paths. -Discovery and classpath each use the session request deadline; the build retains -its independent queue-inclusive deadline. The host fallback deadline covers all -three phases plus event-delivery grace. - -Only the final `requestCompleted` event is published to the host, with either -`{ kind: "ready", target: { mainClass, projectName?, modulePaths, classPaths } }` -or `{ kind: "buildFailed", target, failure: { code, message, report } }`. -Only the two terminal build failures above can produce the latter. -Cancellation, timeout, invalid replies and transport failures remain operation -errors. Cancelling the operation cancels its current phase; a late build answer -retains the existing coordinator semantics and never starts classpath resolution. -Runtime path groups must contain nonblank strings, with at least one path overall. -Hosts save documents before submitting and own the final launch decision, JDK -discovery and native process. Run plan/toolchain selection remain in existing Core -operations. See `rust/fixtures/lsp/java-launch-preparation-v1.json`. + +Deadline cancellations are written by a separate session-owned worker. The +deadline monitor never writes to stdin. A background write that exceeds +`requestTimeoutMilliseconds` fails the session with `transportFailed` at stage +`outboundMaintenance` and terminates the server to release the stalled pipe. The `semanticTokens` operation uses the open document URI and normal version, timeout, and cancellation rules. Its result is @@ -3087,138 +2640,22 @@ newLineCount, additions, deletions }`. `rows` are side-by-side `added`, each changed block listing its removals before its additions. See `rust/fixtures/diff/compare-texts-v1.json`. -`maven.scan` accepts `{ "root": string, "paths"?: string[] }` and returns -`null` when neither the root nor the supplied visible workspace-relative paths -contain a readable `pom.xml`. Candidates are tried in shallowest-first order, -with `/`-normalized lexical paths breaking ties, until one parses successfully; -a malformed candidate does not hide a valid nested project. A project response -contains its workspace `relativePath`, -`groupId`, `artifactId`, `version`, `packaging`, recursive `modules`, `profiles`, -and `hasWrapper`. The root project and every recursive module also contain a -`sourceRoots` list. Each entry has a module-relative `/`-normalized `path` and -a `kind` of `mainJava`, `mainResources`, `testJava`, `testResources`, -`generatedMain`, or `generatedTest`. Standard Maven roots are returned before -their directories exist; explicit `` source/resource directories and -`maven-compiler-plugin` generated-source directories or -`build-helper-maven-plugin` source lists replace the corresponding defaults, -whether configured directly on the plugin or within an execution. -`${project.build.directory}` resolves from `` and defaults to -`target` only when that element is absent; unresolved or invalid explicit -values do not silently fall back. -Absolute, unresolved-property, and parent-traversal paths are omitted so one -module cannot claim another module's source root. Entries are de-duplicated and -ordered by the documented kind order, then path. Aggregator-only `pom` modules -have no default roots. Module paths are relative to the selected Maven root and -use `/` separators. Malformed XML returns `parse_failed`. Source-root examples -are in `rust/fixtures/maven/source-roots-v1.json`. - -`maven.launchPlan` accepts a workspace `root`, a versioned `context`, an -optional reactor-relative `module`, and an ordered `goals` array whose first -entry is a lifecycle or custom goal. Later entries may be ordinary Maven CLI -arguments such as `-Dname=value` or `-q`. They remain separate process arguments -and are never interpreted by a shell. Context version 1 contains the -workspace-relative `reactorPath`, -selected `profiles`, optional platform-local `settingsPath` and -`localRepositoryPath`, `skipTests`, and optional Maven/JDK paths used only for -the configuration fingerprint. The response contains the `project-maven` -toolchain reference, an argument array, the workspace-relative reactor working -directory, and a deterministic SHA-256 configuration fingerprint. Profiles are -sorted and de-duplicated. Module plans from `maven.launchPlan` and Maven-backed -Run and Debug plans use `-pl -am`. Settings use `-s`; a local -repository override uses `-Dmaven.repo.local=`; skipped tests use -`-DskipTests`. Explicit Run `cwd`, Profiles, and `extensions.maven.skipTests` -values override the project context, including `skipTests: false`. The core -never reads `settings.xml` and never copies its path into a portable project -document. Maven itself continues to read `.mvn/maven.config`; the plan does not -expand or duplicate that file's arguments. Fixtures are in -`rust/fixtures/maven/launch-plan-v1.json`. - -`maven.dependencyPlan` accepts the same workspace `root`, versioned `context`, -optional reactor-relative `module`, and a required absolute `outputFile`. It -returns a launch plan for the fixed `maven-dependency-plugin:3.8.1:tree` goal -that writes the verbose text tree to `outputFile` in UTF-8 with standard tree -tokens, disabled color, and an English locale. Exactly one project runs because -every project in the session would overwrite the same file: module queries use -`-pl ` without `-am`, and reactor-root queries use `-N`. The read-only -query does not build reactor dependencies. Platform adapters own the child -process, apply a bounded timeout, and keep it independent from an ordinary Maven -build session. They also own `outputFile`: each invocation receives a fresh -path in a platform scratch directory, and the platform removes it after the -result, cancellation, timeout, or failure. The process's console output is log -text only and is never parsed as dependency data. - -`maven.dependencies` accepts `{ "modulePath": string, "outputFile": string }` -after the plan's process exits successfully and returns the normalized module -path plus a recursively nested `dependencies` array. Each node contains -`modulePath`, `groupId`, `artifactId`, `version`, `type`, nullable `classifier`, -`scope`, `resolution`, nullable `selectedVersion`, nullable -`premanagedVersion` and `premanagedScope` (values before dependency -management), nullable `originalScope` (declared scope before mediation widened -it), nullable `ignoredScope` (a wider scope mediation did not apply), and -`children`. Resolution is `resolved`, `omittedDuplicate`, or `omittedConflict`; -`selectedVersion` is the winning version of an omitted conflict. Every level is -sorted deterministically. - -The first line of the file must name the module and every later line must be a -node with only the annotations the pinned plugin writes; any other content, -invalid UTF-8, a line over 4 KiB, more than 10,000 nodes, 64 levels, or a file -over the byte limit derived from those bounds returns `parse_failed` instead of -a partial tree. A missing file returns `process_failed`. The compatibility -fixture is `rust/fixtures/maven/dependency-tree-v2.json`. - -`maven.diagnostics` accepts `{ "root": string, "output": string }` and returns -`{ "issues": [] }`. Diagnostic paths may be absolute or workspace-relative; -the response preserves the path text, uses one-based line and column values, -and normalizes severity to `error` or `warning`. Duplicate issue lines are -removed deterministically. - -`runConfig.selectJava` uses the toolchain requirements of the workspace's latest -`runConfig.generate` in this process; before any generation it derives them from -the project's own declarations (`pom.xml`, `.java-version`, the Maven wrapper). -Its request contains optional -`root`, `candidates` (`id`, probed `version`, numeric source `priority`), and -`fallbackId`. IDs are opaque machine-local identities, never persisted or opened -by this operation. Lower priority wins; equal-priority candidates use descending -numeric Java versions (including legacy `1.8`), then ascending ID. - -When `project-jdk.minimumVersion` exists, selection first filters using the same -Java version comparison as run-configuration diagnostics. Among the compatible -candidates, the one whose Java feature version equals the declared one wins, else -the oldest newer feature version; source priority only breaks ties within a feature -version. A Java 8 project therefore runs on an installed JDK 8 even when `JAVA_HOME` -points at JDK 21, matching IDEA and OpenIDEA. The response is -`{ id, warning }`: the compatible candidate, or the supplied usable fallback -with an actionable warning if none qualifies. Missing requirements retain the -platform's unconstrained choice; a malformed or unsupported `requirements` -snapshot returns the parse/version error. Explicit configured paths bypass automatic -selection. The operation never generates requirements or probes executables. -The cross-platform examples are in -`rust/fixtures/run-configuration/automatic-java-selection.json`. - -Windows `run_resolve_toolchains` includes an optional `warning` on resolved -JDKs, including inherited Maven JDKs. This warning remains visible even when no -run configuration exists to carry a scoped `toolchainVersionMismatch` diagnostic. - `runConfig.projectToolchain` reads `{root,path,localDocument?,configurationId?}` and returns -`{toolchain:{javaHomePath,mavenExecutablePath,mavenJavaHomePath,runtimeExecutablePaths}}`, even without +`{toolchain:{runtimeExecutablePaths},toolPreferences?,definedTools?}`, even without generated Run configurations. `path` is the current Provider's normalized, -workspace-relative project/reactor root (`.` for the workspace). The nearest -`toolchain.pathOverrides` entry owns all three fields; its empty/missing fields -inherit workspace defaults, not another ancestor override. Matching uses path -segments, so `app` never owns `app2`. Malformed stored paths/documents fail -without changing source bytes. The command does not discover, probe or run any -tool, inspect project descriptors, or write files. `runtimeExecutablePaths` -comes from the existing `.lithe/toolchains/local.json`; platforms resolve the selected -paths through their existing toolchain adapters. Shared examples are in -`rust/fixtures/run-configuration/project-toolchain-selection.json`. -When `configurationId` is supplied, explicit Java/Maven paths from that current -Run configuration override the canonical project/reactor selection. Core reuses -the existing team/local configuration merge without projecting global defaults -as explicit configuration choices. Empty configuration fields inherit the -canonical selection; missing, disabled, mismatched-root or malformed choices fail -instead of falling back. No generated layer is required for an explicitly saved -Provider reference. Shared cases are in -`rust/fixtures/run-configuration/configuration-toolchain-selection.json`. +workspace-relative project root (`.` for the workspace). `runtimeExecutablePaths` +maps each runtime ID in the existing `.lithe/toolchains/local.json` to its saved, +non-empty `executable`; platforms resolve the selected paths through their +existing toolchain adapters. `toolPreferences` echoes the local document-level +`toolchain.preferences`. When `configurationId` is supplied, Core reuses the +existing team/local configuration merge without projecting the global defaults +as explicit configuration choices. The configuration must be enabled and, when +it carries an `extensions.build` reference, belong to the provider instance +rooted at `path`; a `build.provider` configuration then reports its active +`extensions.build-run.tools` choices as `definedTools`. Missing, disabled, +mismatched-root or malformed choices fail instead of falling back, and malformed +stored paths or documents fail without changing source bytes. The command does +not discover, probe or run any tool, inspect project descriptors, or write files. `runConfig.inspect` also returns the local document-level `toolchain`, including when no generated configuration exists (`status: "missing"`). Settings, @@ -3227,14 +2664,14 @@ toolchain-only callers, and initial run-panel presentation may send project sources. Omission preserves full inspection. The Windows run panel publishes readable configurations first, then performs full inspection and reports freshness failures without discarding those configurations. -The input fingerprint covers what generation reads: build and tool files and -the sources of generated Java entries by content (`sha256:` in -`generator.inputs`), and every other Java source by path only (`path`), so -editing a class body is not a staleness. When the stored inputs no longer +The input fingerprint covers what generation reads: the manifests the built-in +detectors consume (package and lock files, Compose files, Procfiles, task-runner +files and runtime version files) by content (`sha256:` in +`generator.inputs`), plus the detector revision. When the stored inputs no longer reproduce the stored fingerprint, the document came from another generator revision and the diagnostic says so instead of listing modified files. -Automatic run discovery, fingerprint traversal, Node manifest discovery and the -Windows Java source list use Core's shared scan policy. Known caches/build outputs +Automatic run discovery, fingerprint traversal and Node manifest discovery use +Core's shared scan policy. Known caches/build outputs are pruned before descending; other hidden source directories remain visible and symlinks are not followed. Failure to read the workspace root is an error. An unreadable optional child or fingerprint input is recorded in @@ -3243,109 +2680,48 @@ hashes beneath those unknown paths. Inspection returns `incompleteProjectScan` instead of claiming fingerprint freshness or inferred removals when either the current scan or saved baseline is incomplete. The persisted diagnostic also appears with `checkFingerprint: false`; a successful complete regeneration clears it. -This does not replace declared build-system modules or JDT's entrypoint facts. -The bounded Java source list stops opening further directories once its source -limit is reached, retains only collected Java paths, and returns them sorted. -Full fingerprint scans remain uncapped by this list limit and keep reporting -unreadable inputs independently. - -`runConfig.inspect` also accepts optional schema-versioned `javaEntrypoints` -from the current `lsp.request` result, independent of `checkFingerprint`. Core -then compares JDT's `(sourcePath, mainClass)` pairs, with a `module/` prefix -removed and nested checkouts excluded, against the generated Java entries and -reports a difference as a `staleFingerprint` diagnostic. Platforms send it -after a project load once the Java service has prepared the project, never -start that service for this check, and skip it while a regeneration already -waits for the service. Project environment saves use the existing local `runConfig.updateOptions` toolchain payload with an empty `configurationId`; service overrides are untouched. The `runConfig.*` commands implement the versioned project protocol described by the JSON Schemas in this directory. `runConfig.inspect` accepts `root` and -never writes files. `runConfig.generate` accepts `root`, relative Java `paths`, -relative `modulePaths`, optional schema-versioned `javaEntrypoints` from the -current `lsp.request` result, and optional `projectOwnership`. The latter is a -host projection of current enabled Provider projects: `{ path, -name?, build?: { pluginID, buildSystemID, root, projectID }, minimumJavaVersion?, contentRoots: [{ path, kind }] }`, using workspace-relative -paths and the model's `source`/`testSource`/generated-source roles. Core validates -project/root containment, unique project paths, roles and bounds (2,000 projects, -10,000 content roots). When supplied, including `[]`, these facts own Java -project membership and source sets: `modulePaths` and ancestor POM inference -do not supplement them. `build` is an exact current model identity, not a build -semantic tag. Generated Java entries store it under `extensions.java.build`, -with Java-owned `module` and `mainClass`; they contain no Maven extension or -Maven tool binding. Its root stays independent of editable cwd and document -layers cannot replace generated build ownership. An omitted snapshot retains the current -Windows generator's legacy ownership path. A supplied ownership snapshot disables -legacy Maven framework detection. Framework selection is not an ownership field; -run discovery uses the independent generic snapshot described below. -`minimumJavaVersion` is an optional normalized Java feature version (1–999), -owned by the upstream compiler model, including before entrypoint discovery -returns any sources. When the snapshot is supplied, Core uses -the highest known project floor and never parses POM compiler properties; a -platform Java version file can raise but cannot lower that floor. Missing means -unknown, not permission to infer. Build-tool versions and wrapper paths are not -ownership fields. With provider ownership Core emits no Maven requirement and -never reads wrapper properties or scripts to supplement it. Installed tool -definitions/providers own these choices. Native Java preparation routes by the -exact enabled package, declaration, instance and project identity. Legacy -consumers that omit provider ownership retain their old Maven launch path until -they migrate; those fields are not accepted in the new ownership projection. See `fixtures/run-configuration/provider-project-ownership.json`. -The generated document records optional `generator.providerFingerprint`, a -SHA-256 identity of the validated projection, with projects and roots sorted. -It covers project labels, ownership, source roles and requirement -facts, but never environment values or native configuration secrets. -`runConfig.inspect` accepts the same optional `projectOwnership` and emits -`staleProviderModel` when current facts differ or a legacy generated document -has no provider identity. `checkFingerprint: false` skips file hashing only; -this model comparison remains active. Explicit `[]` represents disabled/missing -providers, while omission preserves existing Windows callers. A return to the -same semantic facts matches again; asynchronous host publication still needs -its independent monotonic operation revision to reject ABA replies. +optional `checkFingerprint`, `localDocument` and `providerRuns`, and never +writes files. `runConfig.generate` accepts `root`, optional `providerRuns` and +an optional `cacheDirectory`, and returns `{ generated, toolchainRequirements, +entryCount }`. `runConfig.generate` and `runConfig.inspect` also accept optional `providerRuns`: a complete list of `{projectPath,name,reference}`. The native host matches current model targets against enabled public run declarations; `reference` has exactly the -`extensions.build` selector shape below, with empty model `userArguments`. This -snapshot is independent of Java entrypoints and language imports. Core validates +`extensions.build` selector shape below, with empty model `userArguments`. Core validates at most 10,000 unique selectors, provider-root/project containment and bounded labels, then creates `build.provider` entries without interpreting target IDs, tool names, frameworks, commands or debug adapters. An explicit `[]` withdraws generated runs; -omission preserves older callers. It also disables the legacy framework detector. +omission preserves older callers. IDs hash plugin/build/root/run/project/target identity, so changed labels or execution kind retain user overrides. Current declarations still validate the execution kind before preparation. `generator.providerRunFingerprint` hashes the complete sorted snapshot; changed or missing metadata yields `staleProviderModel`, including when file fingerprinting is disabled. Withdrawn entries cannot be restored from cached configurations; unmatched overrides stay in their user-owned layer and are diagnosed. -See `fixtures/run-configuration/provider-project-ownership.json`. macOS supplies this +See `fixtures/run-configuration/build-provider-stable-identities.json`. macOS supplies this snapshot; Windows retains its existing generator until its Provider host is migrated. -Java configurations with the same class and module name in independent reactors -remain distinct; a common reactor retains its existing configuration IDs. - When Java tooling is still preparing, omission -retains the previous generated entrypoint facts instead of running a local -scanner or replacing them with an empty list. Those previous facts come from -this process's last generation, else from the optional `cacheDirectory` (an -application cache root where generation records its result under -`run-configurations/`), else read-only from a `.lithe/run/generated.json` that -older versions wrote. It returns the generated configuration and toolchain -requirement documents and keeps them in memory as the workspace's detected -layer for `inspect`, `resolve`, and `selectJava`; nothing is written into the -workspace. `inspect` returns `requiresGeneration: true` while this process has +Generation keeps the generated configuration and toolchain requirement +documents in memory as the workspace's detected layer for `inspect` and +`resolve`; nothing is written into the workspace. With `cacheDirectory` (an +application cache root outside the workspace) it also records the result under +`run-configurations/`, so retained entries keep their IDs and the Run list is +complete before providers answer again; failing to read or write that cache +changes nothing else. The previous generation, used only to retain entries +beneath unreadable paths, is this process's last generation, else the cached +result, else read-only a `.lithe/run/generated.json` that older versions wrote. +`inspect` returns `requiresGeneration: true` while this process has no detected layer. Hosts identify the workspace when this flag is true or the status is `missing`; a saved provider task can make `status` be `ready` before -detection, and must not suppress discovery of the remaining entries. Without -`projectOwnership`, legacy Maven root -discovery checks `pom.xml` along each supplied path's ancestor chain, so a - -reactor nested below the opened workspace does not depend on the platform -including build descriptors in `paths`. Maven ownership is resolved per Java -entry: standalone sources keep the JDK launch path, while entries from -independent nested reactors retain their own reactor working directory and -module selector. Generated fingerprints include both project inputs and the -detector revision; either changing marks the detected layer stale and requires -regeneration. +detection, and must not suppress discovery of the remaining entries. Generated +fingerprints include both project inputs and the detector revision; either +changing marks the detected layer stale and requires regeneration. `runConfig.decideTitleAction { action, trigger, currentFileChosen, settled, configuration? }` decides what a press on the title bar's Run or Debug button does. `action` is @@ -3392,20 +2768,6 @@ states, the effective global `toolchain`, and the machine-local `localToolchains` document. Toolchain diagnostics carry the affected run configuration ID when a requirement is consumed by one or more configurations; requirements with no configuration consumer do not emit a blocking diagnostic. -For detected Maven configurations, resolved `extensions.maven.reactorPath` -contains the workspace-relative reactor from the generated layer, independently -of an overridden effective `cwd`. Core derives this read-only ownership value -when resolving existing generated documents as well; regeneration is not -required. Overrides cannot move a configuration to another reactor. Current -File and configurations without detected Maven ownership omit this field. -Resolution checks that `extensions.maven.module` exists relative to this -reactor, or relative to `root` when the field is absent, and never relative to -an overridden `cwd`: setting a module's own directory as the working directory -keeps the configuration available. -Module menus first match reactor and module, then apply the default preference; -they must not infer ownership from an overridden working directory. The shared -`run-configuration/maven-module-ownership.json` fixture covers independent -reactors, cwd overrides, and the ordinary Java main / Current File capabilities. Each configuration carries a `category` of `project` or `infrastructure`. Docker Compose detections are `infrastructure`: a Compose file in an application @@ -3421,32 +2783,24 @@ selection filtering are pending there. Display names that repeat are qualified by Core, because hosts show the name alone: the first candidate that separates every entry in the group wins, trying -the Maven module, then the working directory, then the source manifest. Three +the working directory (when it is not the workspace root), then the source +manifest. Three Compose files each declaring `compose up` become `compose up (script/docker)` and so on, while a name that occurs once is never decorated. Ids are unaffected. -Java entry points are read from the Java syntax tree rather than matched as -text, so a `static void main` or `@SpringBootApplication` inside a string -literal or comment — common in test fixtures and documentation samples — does -not become a run configuration. A declared `main` under `src/test` remains a -valid entry and keeps the test classpath; see -`rust/fixtures/execution/maven-java-main-source-sets-v1.json`. - A process detector declares a runtime binding only when that command genuinely consumes the runtime. npm, pnpm, and Yarn scripts consume `project-node`; Bun scripts keep their independent `bun` command and do not acquire a Node -requirement. Go, Python, Cargo, and Gradle remain command-based until every host +requirement. Go, Python, and Cargo remain command-based until every host provides the corresponding configurable runtime registry, so their PATH-based launch behavior is not blocked by an unavailable platform selector. During resolution, Core also reconciles npm, pnpm, and Yarn commands from older v2 generated documents with the same `project-node` binding. This compatibility normalization is based on the effective command, does not mutate the stored document, and keeps legacy hybrid projects scoped without requiring regeneration. -A document-level `toolchain` -object in the local layer (e.g. -`{ "java": { "homePath": ... }, "maven": { "executablePath": ..., "javaHomePath": ... } }`) -provides defaults for every configuration's `extensions.java.*`. A non-empty -per-configuration toolchain path overrides the corresponding project default. +The document-level `toolchain` object in the local layer holds generic +`preferences`; other fields there belong to whichever host or plugin wrote them, +and Core preserves them unchanged. Provider applications can be explicitly saved with `runConfig.createUserConfiguration` using `{root,scope:"local",name,type:"buildProvider",build:{pluginID,buildSystemID, @@ -3524,37 +2878,20 @@ before process launch, and an invalidated configuration or operation rejects lat `runConfig.updateOptions` and `runConfig.createUserConfiguration` are pure document transformations. They validate scope, paths, supported types, stable -IDs, main classes, modules, and argument parsing, then return UTF-8 JSON in the +IDs, provider run options, and argument parsing, then return UTF-8 JSON in the `document` field. The platform adapter selects the target project or local file and performs the atomic write. These commands never write files. An empty `workingDirectory` removes the layer's `cwd` override. A non-empty value must name an existing directory inside `root`, given relative to it or as an absolute path, and is stored project-relative. Values are literal paths; editor variables such as `${workspaceFolder}` are rejected rather than stored, because -resolution disables and omits a configuration whose `cwd` does not exist. Optional -`mavenSkipTests` writes `extensions.maven.skipTests`; omission removes the -override so the project Maven context is inherited, while explicit `false` -continues to run tests even when the project default skips them. -For project-scoped option updates, selected toolchain paths must resolve inside -`root` and are persisted with `/`-separated project-relative paths. Local-scoped -updates may carry host absolute paths. `runConfig.updateOptions` and -`runConfig.inspect` accept the same optional `localDocument` override. -When `updateOptions` carries a `toolchain` object (`javaHomePath`, -`mavenExecutablePath`, `mavenJavaHomePath`), it writes the document-level -global toolchain into the local layer instead of patching a configuration; -project scope rejects this payload because toolchain paths are machine-local. - -The project toolchain may also carry `pathOverrides`, a map of normalized -workspace-relative subproject roots to `{ javaHomePath?, mavenExecutablePath?, -mavenJavaHomePath? }`. The map has at most 256 entries; roots reject absolute, -parent, dot, empty-segment, drive, backslash and control-character forms. Empty -fields inherit project defaults. This map lives only in `.lithe/run/local.json` -and is returned by inspection/resolution; hosts preserve it when saving a run -configuration or project defaults. An absent map identifies a document predating -subproject migration; a saved empty map is an explicit choice. Old platform -preferences are copied and read back before cleanup, with already saved choices -remaining authoritative. See `rust/fixtures/run-configuration/subproject-toolchains.json`. - +resolution disables and omits a configuration whose `cwd` does not exist. +`runConfig.updateOptions` and `runConfig.inspect` accept the same optional +`localDocument` override. When `updateOptions` carries a `toolchain` object +(`{ runtimeExecutablePaths?, preferences? }`), it replaces only the +document-level `toolchain.preferences` in the local layer instead of patching a +configuration, preserving every other document-level toolchain field; project +scope rejects this payload because toolchain choices are machine-local. `execution.resolveSearchPath` accepts already-split `existing` directories and `candidates` carrying `priority`, opaque `id`, and `path`; lower priority sorts @@ -3570,8 +2907,8 @@ expansion or execution. Empty arguments remain explicit. The shared cases are in `rust/fixtures/run-configuration/argument-text.json`. `runConfig.saveEditorChanges` accepts the normal option-edit payload plus the -required `toolchain` object. In addition to Java and Maven paths, that object -may contain `runtimeExecutablePaths`, keyed by stable generic toolchain ID. It +required `toolchain` object. That object may contain `preferences` and +`runtimeExecutablePaths`, keyed by stable generic toolchain ID. It applies the global toolchain and configuration override edits together, returning `localDocument`, either a `projectDocument` string or `null`, and either a `toolchainDocument` string or `null`. The latter updates @@ -3579,9 +2916,7 @@ either a `toolchainDocument` string or `null`. The latter updates an entry when its supplied executable path is empty. Local scope combines the run-option edits in the local run document. Project scope returns the local defaults and team options as separate fully prepared documents; the platform -adapter writes all returned documents as one transaction with rollback. Empty -per-configuration toolchain paths remove the -corresponding override keys while preserving unrelated extension fields. +adapter writes all returned documents as one transaction with rollback. Platform clients report the editor save as successful only after the written documents resolve again. Failures identify whether preparation, document writing, or post-save reload failed; a reload failure keeps the last usable UI @@ -3597,177 +2932,20 @@ selected Node installation. They do not fall back to a PATH shim from another installation, and Windows executable extensions take precedence over extensionless shell scripts. -Automatic Spring service generation requires both an applied Maven plugin and -exactly one JDT-confirmed, Spring-classified non-test entry in the same module. -An inherited plugin alone cannot promote a library to a runnable service. -Nested-module entries do not promote their parent. Ambiguous, utility and test -mains remain independently selectable Java entries. Module assignment consumes -the confirmed source set even when the caller supplies no editor paths. -Absent discovery may reuse previously confirmed entries; an explicit empty result -removes automatic services. Existing service IDs and user-authored Maven-goal -compatibility configurations retain their established meaning. - -`runConfig.createLaunchPlan` accepts `root`, `configurationId`, optional -`currentFile` and `classPath`, optional `debugPort`, and optional -`localDocument`. Maven-backed Run and Debug callers may also supply the same -versioned `mavenContext` accepted by `maven.launchPlan`. Explicit profiles in -the resolved Run Configuration replace the context profiles; otherwise the -project profiles are inherited. Explicit `extensions.maven.skipTests` and -`cwd` values also replace the context values. Core applies the shared settings, -module, Skip Tests, and reactor-working-directory rules to the generated -framework arguments, including `-am` for selected reactor modules. A -project-owned `java.main` caller, or a `spring-boot.maven` caller whose generated -configuration records a Java entry source, instead supplies `javaLaunch` with -the exact JDT LS-selected `mainClass` plus structured `classPaths` and -`modulePaths`. An optional `javaLaunch.buildPrepared: true` is a host receipt -that the current build provider resource task completed before JDT launch -resolution. Core then omits its legacy Maven resource pre-launch step and does -not inspect POMs for that step. The host must recheck workspace/configuration -generation and cancellation before handing off the launch; it cannot reuse a -receipt from another operation. Missing/false retains compatibility for current -Windows callers. See `fixtures/run-configuration/provider-prepared-java.json`. Core then produces a direct `project-jdk` launch; it never -projects those Java entry points into a reactor-wide Maven launch goal. A Spring -Boot configuration without a resolved Java entry source retains its Maven-goal -compatibility path. It -returns a toolchain -reference, argument array, project-relative working directory, and structured -environment references. It does not return a shell command or platform -executable path. All project paths use `/`, reject absolute paths and `..` -traversal, and remain relative to `root`. - -The plan may also carry three optional envelope fields. `preLaunchSteps` is an -ordered array of `{ executable, arguments, classpath?, workingDirectory? }` steps -the host runs to completion, in order, before the main process; a non-zero exit -aborts the run and surfaces that step's diagnostics. Each step's `executable` -reuses the plan's -`{ toolchain }` shape plus an optional `tool` selector (`"javac"` resolves the -sibling compiler in the toolchain's `bin` directory; absent means the default -launcher). `classpath` is a structured array of project-relative or host-absolute -entries the host joins with the platform path separator (`:` on POSIX, `;` on -Windows) and prepends as `["-cp", joined]` to the relevant argument list; core -never joins classpath entries because the separator is platform-specific. -`modulepath` follows the same rule and is emitted as `--module-path` by the -host. A JDT main identity in `module/name.Type` form is projected as -`-m module/name.Type` for the direct Java launcher. Empty fields are omitted, -so existing single-process Maven, Gradle, and Node -plans are unchanged. A step shares the plan-level `environment` and, unless it -declares its own project-relative `workingDirectory`, the plan-level -`workingDirectory`. A step that declares one resolves its toolchain from that -directory and runs there, so a resource step still finds the project wrapper -next to its reactor POM when the application working directory is a user -override. - -Direct Maven-project Java launches also carry a `project-maven` resource step: -`resources:resources`, plus `resources:testResources` for test-source entrypoints. -The step inherits Maven profiles, settings, repository and module/dependency -selection. Its absolute `-f` POM argument anchors the generated reactor even when -the application working directory is overridden, and its own `workingDirectory` -names that reactor as the step's run directory and toolchain resolution root. A -child POM not declared in -that reactor is processed as its own Maven project, without `-pl`. Resource -filtering and custom resource directories remain Maven-owned; no Java compilation -or application launch goal is added. Hosts must finish these steps successfully -before starting the JVM. Both hosts scope pre-launch execution to the -window/session/execution and stop the owned process tree on Stop, window close or -replacement. They apply the same ten-minute deadline with bounded pipe draining -and cleanup, and a step that reaches its deadline fails the run with -`Pre-launch step timed out after 600 seconds.`. macOS runs the steps before both -the application launch and a Run-panel service session, and cancels a session's -running step when that session stops, restarts, or is dropped by reconciliation. -Independent DAP launches that do -not consume this plan are not covered by this resource-step contract. - -A Maven-project `java.main` launch must first ask JDT LS/Java Debug -Server to resolve the exact source target, build its owning project, and return -the runtime classpath/module path. Missing project launch metadata is a launch -error rather than permission to fall back to `mvn exec:java`; reactor-wide Exec -would attempt the same main class in parent and dependency modules. - -A `java.main` configuration without a -Maven toolchain launches through `project-jdk` and the configuration's Java -source path only when that source has no Maven ancestor. An older configuration -that omitted the Maven binding is rejected with an instruction to regenerate. -Standalone Java (`java.main` without Maven, and `java.current-file`) compiles -before running: core emits a `javac` pre-launch step writing `.class` files to -`.lithe/run/classes/`, puts that directory on the run -`classpath`, and launches by the qualified class name rather than the source -file. This keeps one-click Run working on JDK 8, which lacks the JEP 330 -single-file source launcher (`java File.java`) that only exists on JDK 11+. The -`java.current-file` main class is derived from the file's `package` and declared -class; when the host supplies a project `classPath`, the compiled output leads -the run classpath and the project classes feed the compile step. - -`java.codeVision` accepts a workspace root, a target Java path, and Java source -paths. It returns declaration locations and usage counts; Git blame attribution -is joined by the UI from the shared Git result. `java.className` accepts Java -source text and a file simple name and returns the fully qualified runtime class -name. - -`java.sourceDefinition` accepts `source`, `declarationName`, an optional -`memberName`, an optional `declarationsOnly`, and an optional `reference` -(`line`, `utf16Column`), returning zero-based `line` and UTF-16 `utf16Column` or -`null` when no declaration is found. A member is looked up as a syntax-tree -method, field, or enum-constant declaration that the `declarationName` type -declares directly. With `reference`, the member is instead resolved from that -position: the identifier must be a plain name or a `this.` access, the innermost -type is searched first and then the enclosing ones, and the result is `null` when -a type on the way may inherit the name or a local may shadow a field. -Without `declarationsOnly`, text that merely resembles a declaration, such as a -call, is accepted next, and then the `declarationName` type; with it, a missing -member returns `null`. - -`java.structure` accepts Java `source` and optional `declarationSources`. It -returns `foldRegions`, `inlayHints`, and `syntaxHighlights`. -Fold and inlay line numbers are zero-based because these values are editor -offsets; UTF-16 columns and hidden ranges match the native text editor coordinate -system. Syntax highlights contain document-relative `utf16Start`, -`utf16Length`, and a role from the shared editor syntax-theme contract. They -are sorted and non-overlapping, so native renderers can apply semantic colors -without maintaining another Java parser. The parser is platform-independent -and does not start a Java process or contact JDT. - -`spring.index` accepts `root`, workspace-relative `paths`, optional trusted -absolute `metadataRepositories` (and the legacy singular `metadataRepository`), -optional `textOverrides` keyed by relative path, and -`refreshDependencyMetadata`. The command reads Spring configuration -metadata from workspace JSON files and dependency JARs, indexes application -configuration documents and Java source, and returns deterministically ordered -`properties`, `values`, `propertyReferences`, `diagnostics`, `beans`, -`injections`, and `endpoints` collections. Locations use relative paths and -one-based lines and columns. - -`properties` include type, documentation, default value, and an optional Java -declaration. `values` include profile/override state and an optional declaration -target. `propertyReferences` represent Java `@Value` uses. Bean resolution -accounts for component names, `@Bean` aliases, interfaces, `@Qualifier`, -`@Resource`, `@Primary`, field injection, and constructor injection. Endpoint -entries expand multiple controller/method paths and retain the exact declared -HTTP method set. - -Dependency metadata is cached in the Rust process. Project-open indexing sets -`refreshDependencyMetadata` to `true`; debounced unsaved-buffer indexing leaves -it `false`, so editing Java or configuration files does not repeatedly traverse -and open the local dependency repository. The repository path is selected by -the platform composition layer and is never persisted in shared results. - -`mybatis.index` accepts `root`, workspace-relative `paths`, and optional -`textOverrides` keyed by relative path. It pairs Java mapper types with XML -`` documents and returns only statements that have both a -Java method and a matching XML `select`/`insert`/`update`/`delete` `id`. -Results are deterministically ordered by namespace, statement id, XML path, -and line. Locations use relative paths and one-based lines and columns. -`javaLine`/`javaColumn` point at the method name; `javaEndColumn` is the -exclusive UTF-16 column after that name. `javaEndLine` is the signature -terminator. `xmlLine`/`xmlColumn`/`xmlEndColumn` bound the statement `id` -value the same way. Hosts intercept go-to-definition only when the caret -is inside those name ranges; return types and parameters keep LSP -navigation. Java methods are collected from `tree-sitter-java` syntax -nodes, so nested generics, split signatures, and commented-out methods -are not mistaken for declarations. XML comments are ignored. Methods with -a method body, including `default` methods, are omitted from the Java -side of the index. Paths are indexed only when they are regular `.java` -or mapper `.xml` files no larger than 2 MiB; `pom.xml` and other -extensions are skipped before content is read. +`runConfig.createLaunchPlan` accepts `root`, `configurationId` and optional +`localDocument`. It resolves the configuration through the same layers and +rejects a missing or disabled one. Core assembles only process-based plans: a +configuration with a bare program-name `command` returns +`executable: { command }`, which the host resolves on PATH; one that transfers +executable ownership to a runtime toolchain (`toolchains.runtime`) returns +`executable: { toolchain }`, which the host resolves from its toolchain +registry. Any other configuration, including a `build.provider` reference, is +`invalid_request`: ecosystem launch semantics stay with the plugin that +declared the run. The plan also returns the string argument array, the +project-relative `workingDirectory`, an empty `environment` reserved for +toolchain-derived values, and the configuration's literal `env`. It does not +return a shell command or platform executable path. All project paths use `/`, +reject absolute paths and `..` traversal, and remain relative to `root`. `diagnostics.redactText` accepts `text` and returns `redacted` with credentials, tokens, and home-directory paths replaced by stable placeholders @@ -4501,8 +3679,9 @@ entry?, runtimeArguments, arguments }` and returns `{ status: "ok", launch: params }`, where `params` are those of `lithe/host/run/start` as the backend sent them. It returns `{ status: "ok", start }` with `start` being `{ label, executable: { kind: "toolchain" | "packaged", value }, arguments, -workingDirectory, environment, temporaryFiles: [{ path, contents }], debug?, -rerun? }`. The tool must be listed in the manifest's `requiresTools`, and the +workingDirectory, environment, temporaryFiles: [{ path, contents } | { path, +base64 }], debug?, rerun? }`. A text file is written as UTF-8; a `base64` file +holds bytes the host decodes and writes unchanged. The tool must be listed in the manifest's `requiresTools`, and the working directory must be a relative path of an existing directory inside `workspaceRoot`, which Core checks read-only the way it does for run preparation. `${temporaryFile:}` in arguments and environment becomes a file path under @@ -4575,12 +3754,12 @@ failure is terminal. There is no separate `configuredExecutable` result. See the plugin contract's **Shared executable candidates** section for ordering and strict selection. -Host-managed runtime probes use `java -version` (including Java 8), -`node --version`, and `go version`. Hosts execute the returned arguments and -pass combined stdout and stderr as `build.checkToolVersion.request.output`: -Java normally prints its version to stderr. Core normalizes legacy Java -`1.8.0_…` output to major version 8 before comparing the declared minimum, -and ignores `Picked up …` environment-option notices preceding the version. +Host-managed version probes run the arguments of the tool definition's +`version` declaration. Hosts execute them and pass combined stdout and stderr +as `build.checkToolVersion.request.output`, because some tools print their +version to stderr. The definition's `version.pattern` alone captures the +version, including any tool-specific numbering such as a legacy `1.` prefix; +Core never branches on a tool name. Paths in requests are workspace-relative with `/` unless named absolute. `root` is the instance root from detection. Selected entities are @@ -4615,7 +3794,6 @@ and the directories are absolute host paths used only as substitution text. | `build.decodeDiagnostics` | `{ workspaceRoot, root, output }` | `diagnostics`: `[{ path, line, column?, severity, message }]`, deduplicated, inside the workspace only | | `build.decodeDependencies` | `{ workspaceRoot, root, project?, output?, outputFile?, result? }` | `dependencies`: `{ project, dependencies: [{ name, group?, version?, scope?, properties?, children }], truncated }` | | `build.syncFingerprint` | `{ workspaceRoot, root, projects, options? }` | `sync`: `{ fingerprint, watch }` | -| `build.languageImport` | `{ workspaceRoot, root, options?, executablePath? }` | `languageImport`: `null` without a `languageImport` declaration; `{ kind: "maven", mavenContext: { version, reactorPath, profiles, settingsPath?, localRepositoryPath?, mavenExecutablePath? } }` for `lsp.startServer.mavenContext` and `lsp.updateMavenConfiguration`; or `{ kind: "gradle", javaGradle: { wrapperPresent, offline, userHome? } }` for `lsp.startServer.javaGradle`, where `wrapperPresent` reports `gradle/wrapper/gradle-wrapper.properties` under `root` | Invocation candidates also include `{ kind: "toolchain", toolchainID, root }`. Hosts resolve them through the same canonical project/subproject @@ -4636,6 +3814,12 @@ inside `workspaceRoot` (and the given `outputFile`); none of the commands writes anything or starts a process. `shared/plugins/contract/v1/fixtures/build-systems/build-plans.json` holds the golden plans and decoded outputs. +A `junitXml` testcase's optional `durationMillis` comes only from its `time` +seconds attribute, rounded to whole milliseconds. Repeated invocations of one case +sum their times; when any invocation lacks a valid non-negative time, or the sum +exceeds 2^53 - 1, the field is omitted. A reported zero is valid. Hosts hide a +missing duration and never substitute a suite or console summary. + ### VCS providers The `vcs.*` commands implement the `vcsProviders` capability of @@ -4909,32 +4093,6 @@ than being inserted as literal source text. The initialize handshake advertises incomplete items. Core forwards `labelDetails` and omits null `data` so `completionItem/resolve` can still produce import `use`/`import` edits. -The shared JDT adapter sends `java.completion.guessMethodArguments: "off"` -through initialization, configuration notifications and section queries on both -platforms. JDT LS therefore leaves constructor and method argument lists empty -while preserving the snippet cursor inside the parentheses. Other completion -settings declared in the provider's initialization options are retained for the -session in initialize, didChangeConfiguration, and parent/leaf section pulls -(including favoriteStaticMembers and filteredTypes). This does not disable snippet -support or strip parameter text from arbitrary completion items; the shared -configuration fixture is `rust/fixtures/lsp/java-completion-v1.json`. - -### Java preparation snapshot - -Java `projectPreparation` runtime events carry a `result` object with `phase` -(`starting`, `importing`, `configuring`, `building`, `ready`, `stopped`), `status` -(`idle`, `loading`, `ready`, `failed`) and boolean `blocksRun`. -`lsp.pollEvents` and `lsp.waitEvents` also return `projectPreparation` (the current -snapshot or null) alongside `events`, so restored consumers need not replay the -queue. Event session identity and sequence retain their existing semantics. - -The snapshot reuses the existing service-ready signal, profile-task results and -configuration/build coordinator. Generic indexing never blocks Run. Profile -failure is visible but does not globally block unrelated targets; callers still -build the selected target before launching. A successful preparation does not -promise compilation success. Shared examples live in -`rust/fixtures/lsp/project-preparation-v1.json`. - ### Workspace commit preconditions `git.commitState` accepts `{ root }` and returns `{ head, branch, indexEntries, @@ -5283,26 +4441,26 @@ application action. Closing the connection releases the callback. Windows delive window-scoped Tauri events rather than an ordered Channel; a missing notification must not block subsequent requests. -### Java service hot replacement +### Hot code replacement -`debug.inspect` accepts the Java-provider extension `kind: "redefineClasses"` -with a caller-owned `operationId`. Unlike the inspection kinds, this operation -**mutates the running debuggee**; it requires a running or paused Java session, -but no selected thread. Platforms save documents and complete a successful JDT -`vscode.java.buildWorkspace` before submitting it. They must retain the original -launch target and compare its runtime paths with JDT before compiling; changed -paths require a restart instead of applying to a different output directory. +`debug.inspect` accepts `kind: "redefineClasses"` with a caller-owned +`operationId` only for a session whose adapter declares +`features.hotCodeReplace` in its plugin declaration; any other session is +`invalid_request`. Unlike the inspection kinds, this operation **mutates the +running debuggee**; it requires a running or paused session, but no selected +thread. Core sends the declared `request` command and relays the declared +`event` messages to the Debug Console as progress. Building the changed code +before submitting the operation belongs to the plugin that owns the language; +Core never compiles, and never interprets the replaced units. The terminal result is `{ kind: "redefineClasses", changedClasses: [...] }`, -sorted and deduplicated. Empty means no classes were replaced. Java Debug Server's -`errorMessage` inside a successful DAP response becomes `operationFailed` with -`adapterRejected`; the debug session remains usable. Malformed replacement -results also fail only the operation. Replacement does not imply continue. -The fixture is `rust/fixtures/debug/hot-code-replace-v1.json`. - -Java debug launches append `-Dspring.devtools.restart.enabled=false` (also for -Core-planned direct JDT JDWP launches) so DevTools cannot restart the classloader during -HotSwap. Attach to independently launched JVMs does not change their options. +read from the declared `changedField`, sorted and deduplicated; without a +declared field it is empty. A non-empty message in the declared `errorField` of +a successful DAP response fails the operation; the debug session remains +usable. Malformed replacement results also fail only the operation. +Replacement does not imply continue. The fixture is +`rust/fixtures/debug/hot-code-replace-v1.json`. + The Windows host maps `redefineClasses` to this operation and `cancelOperation` to `debug.cancelOperation` with a `timedOut` reason for its bounded result wait. @@ -5428,19 +4586,6 @@ presentation feature; views never call the C ABI. Windows can reuse validation when it later adds a native renderer; Core support does not make Windows advertise native plugin content. -### Project JDK supplied to JDT LS - -`lsp.startServer.javaRuntimes` entries contain `homePath`, `version`, and optional -`isDefault` (default `false`). Platforms use their existing project toolchain selection, -including explicit JDK homes outside discovery roots. The server process JVM remains -independent. Core registers one JDK per Java major; the first valid selected candidate -wins its major and alone receives `default: true` in `java.configuration.runtimes`, -both at initialization and in configuration responses. Empty homes and unparseable versions -are ignored during projection; platform adapters validate explicit choices before launch. -Legacy callers that omit `isDefault` retain their previous behavior. -Fixture: `rust/fixtures/lsp/java-project-runtimes-v1.json`. - - ### Workspace indexing policy `workspace.configureIndexing` accepts `root`, `manualExcludedDirectories`, @@ -5526,8 +4671,7 @@ in force. Hosts must display this state instead of treating it as empty rules. Tree listings deliberately remain available. Snapshot inventories, filename and content indexes, symbols and replacement preview candidates obey the policy. Generated filename search cannot bypass it. Language-server working-copy sync is -blocked for excluded paths. Java additionally receives import exclusions; this does -not promise resource-scan exclusion for imported or build-declared modules. Rust and Go receive native configuration overlays that preserve user settings; +blocked for excluded paths. Rust and Go receive native configuration overlays that preserve user settings; other providers may continue background scanning and hosts must display that limitation. `workspace.snapshot.incompleteDirectories` is present when optional child directories @@ -5624,8 +4768,8 @@ scope covers nothing and an empty prefix is `InvalidRequest`. It returns others stay in place, then the run's covered results follow. Incoming results outside the scope are discarded, and for a repeated id the later entry wins. Core compares strings only: hosts keep the result payloads, and the provider -adapter builds the scope (for example turning Java nested classes `Outer$Inner` -into the prefix `Outer$`). Cases: `rust/fixtures/build/test-outcome-merge-v1.json`. +adapter builds the scope (for example turning a parent result `suite` into the +prefix `suite/`). Cases: `rust/fixtures/build/test-outcome-merge-v1.json`. ### Variable and disassembly paging @@ -5726,17 +4870,12 @@ does not transition session state. Codes have one meaning: | code | stage | meaning | | --- | --- | --- | -| `mavenSettingsUnchanged` | `mavenSettings` | Maven settings could not be normalized and were forwarded unchanged | -| `configurationCleanupFailed` | `configurationCleanup` | Unused language-server configuration areas could not be removed | -| `mavenProfileStartFailed` | `mavenProfileStart` | The pending Maven profile update could not start | - -The existing Core log sources additionally classify `upstreamStderr` (stderr -observation), `stderrReadFailed` (stderr I/O), `navigationLimited` (batch cap), -`navigationMarkerFailed` (marker verification), `workspaceImportStalled` -(no import progress), `mavenProfileUpdateFailed` (project update failure), -`shutdownTimeout` (forced termination after shutdown deadline), and -`mavenProfileTimeout` (profile application deadline). Each code names exactly -that observation; arbitrary upstream messages retain `upstreamDiagnostic`. +| `upstreamStderr` | `stderr` | The language server wrote to stderr | +| `stderrReadFailed` | `stderr` | Reading the language server's stderr failed | +| `shutdownTimeout` | `shutdown` | The shutdown deadline passed and Core forced termination | +| `processCleanupFailed` | `cleanup` | The language-server process could not be cleaned up | + +Each code names exactly that observation. Hosts persist these controlled identities, never the raw detail, URL, output or argv. @@ -5782,7 +4921,7 @@ of the validated catalog routing field. Hosts must use `fields` and `channel` from Core, never rebuild the route from `providerId` or `pluginId`. Log runtime events retain `type:"log"`, sequence, providerId, sessionId, -level, message, detail, error, and Maven state fields. They add `channel`, +level, message, detail and error. They add `channel`, `pluginId?`, `role`, `event` (catalog ID), `catalogLevel` (canonical uppercase v2 level), and `fields` (normalized scalar disk fields). `type` is the actual JSON discriminator; there is no `kind` wire key. Catalog metadata determines canonical v2 severity; the old `level` remains a v1 @@ -6098,22 +5237,6 @@ the failure again. {"ts":"2026-11-03T08:00:01.123Z","level":"WARN","event":"vcs.git.command.failed","channel":"vcs/git","op":"op-7f3a","fields":{"command":"fetch","arguments":"fetch --prune origin","repository":"/src/app","source":"user","duration_ms":311,"exit_code":128,"stderr":"fatal: could not read from remote repository."}} ``` -`agent.attachments.retentionPlan` decides which pasted Agent images the platform may -delete; it reads and writes no files. The payload is `{nowMs, entries, acceptedReferences, -ledgerPresent, caseInsensitive}`: `entries` lists the platform's pasted-image cache as `{path, modifiedMs, -abandoned}` (`modifiedMs` is Unix milliseconds and may be omitted; `abandoned` marks a -paste that failed or was discarded before any conversation accepted it), -`acceptedReferences` is the platform ledger of paths a conversation accepted, and -`caseInsensitive` (Windows) compares paths without case; separators are always normalized. -`ledgerPresent` reports only whether the ledger file exists, never whether it is empty: when it -is missing, every non-abandoned cached image counts as accepted (the cache predates the -ledger) and is returned in `seed`, which the platform writes atomically (temporary file, then -rename) before deleting anything. The result is `{delete, keep, seed}`, the first two lists of `{path, reason}` in entry order. Reasons: -`referenced` (kept, even when flagged abandoned), `recent` (kept, unreferenced and at most -seven days old or of unknown age), `abandoned` and `expired` (deleted). The cache and the -ledger live in the platform cache directory, never in the app bundle or install directory. -The fixture is `rust/fixtures/agent/attachment-retention-v1.json`. - ### Atomic local toolchain pin editing `toolchains.editPins` accepts `{document,directory,edits:[{tool,pin}]}` and returns diff --git a/rust/contracts/toolchain-local-v1.schema.json b/rust/contracts/toolchain-local-v1.schema.json index c39dd18a..15ce1360 100644 --- a/rust/contracts/toolchain-local-v1.schema.json +++ b/rust/contracts/toolchain-local-v1.schema.json @@ -7,15 +7,32 @@ "properties": { "version": { "const": 1 }, "toolchains": { + "description": "Legacy entries written by earlier releases. Core keeps them unchanged and never interprets their keys.", "type": "object", + "maxProperties": 1024, "additionalProperties": { "type": "object", - "properties": { - "home": { "type": "string", "minLength": 1 }, - "executable": { "type": "string", "minLength": 1 }, - "javaHome": { "type": "string", "minLength": 1 } - }, - "additionalProperties": false + "maxProperties": 16, + "additionalProperties": { "type": "string", "maxLength": 4096 } + } + }, + "pins": { + "description": "Workspace-relative directory ('.' for the root) to tool ID to the installation pinned there.", + "type": "object", + "maxProperties": 1024, + "additionalProperties": { + "type": "object", + "maxProperties": 64, + "additionalProperties": { + "type": "object", + "required": ["home", "version", "provider"], + "properties": { + "home": { "type": "string", "minLength": 1, "maxLength": 4096 }, + "version": { "type": "string", "maxLength": 128 }, + "provider": { "type": "string", "maxLength": 256 } + }, + "additionalProperties": false + } } } }, diff --git a/rust/contracts/toolchain-requirements-v1.schema.json b/rust/contracts/toolchain-requirements-v1.schema.json index 5eedb378..0a316112 100644 --- a/rust/contracts/toolchain-requirements-v1.schema.json +++ b/rust/contracts/toolchain-requirements-v1.schema.json @@ -19,9 +19,7 @@ "type": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" }, "minimumVersion": { "type": "string", "minLength": 1 }, "preferredVendor": { "type": "string", "minLength": 1 }, - "wrapper": { "type": "string", "pattern": "^(?!/)(?!.*(^|/)\\.\\.(?:/|$)).*$" }, - "version": { "type": "string", "minLength": 1 }, - "java": { "type": "string", "minLength": 1 } + "version": { "type": "string", "minLength": 1 } }, "additionalProperties": false } diff --git a/rust/fixtures/build-project-options/shared-defaults.json b/rust/fixtures/build-project-options/shared-defaults.json index fc63a3ca..557e3944 100644 --- a/rust/fixtures/build-project-options/shared-defaults.json +++ b/rust/fixtures/build-project-options/shared-defaults.json @@ -1,7 +1,7 @@ { "version": 1, "systems": { - "maven": { + "make": { "options": {}, "instanceDefaults": { "profiles": [ diff --git a/rust/fixtures/build-project-options/validation.json b/rust/fixtures/build-project-options/validation.json index b300e01a..57a51ae8 100644 --- a/rust/fixtures/build-project-options/validation.json +++ b/rust/fixtures/build-project-options/validation.json @@ -3,23 +3,23 @@ "cases": [ { "name": "empty document", "valid": true, "document": {"version": 1, "systems": {}} }, { "name": "numeric schema const", "valid": true, "document": {"version": 1.0, "systems": {}} }, - { "name": "per root empty selection and inactive custom choice", "valid": true, "document": {"version": 1, "systems": {"maven": {"instanceDefaults": {"profiles": ["ci"], "offline": false}, "instances": {"services/项目": {"profiles": []}, ".": {"settingsFile": "selected.xml"}}, "savedChoices": {"profiles": ["ci", "manual"]}}}} }, + { "name": "per root empty selection and inactive custom choice", "valid": true, "document": {"version": 1, "systems": {"make": {"instanceDefaults": {"profiles": ["ci"], "offline": false}, "instances": {"services/项目": {"profiles": []}, ".": {"settingsFile": "selected.xml"}}, "savedChoices": {"profiles": ["ci", "manual"]}}}} }, { "name": "unknown system retained", "valid": true, "document": {"version": 1, "systems": {"future-tool": {"options": {"custom.option": "saved"}}}} }, { "name": "boolean version", "valid": false, "document": {"version": true, "systems": {}} }, { "name": "unsupported version", "valid": false, "document": {"version": 2, "systems": {}} }, { "name": "unknown document field", "valid": false, "document": {"version": 1, "systems": {}, "extra": true} }, - { "name": "unknown system field", "valid": false, "document": {"version": 1, "systems": {"maven": {"extra": true}}} }, - { "name": "numeric option", "valid": false, "document": {"version": 1, "systems": {"maven": {"options": {"threads": 2}}}} }, - { "name": "null option", "valid": false, "document": {"version": 1, "systems": {"maven": {"options": {"profiles": null}}}} }, - { "name": "mixed list", "valid": false, "document": {"version": 1, "systems": {"maven": {"options": {"profiles": ["ci", false]}}}} }, + { "name": "unknown system field", "valid": false, "document": {"version": 1, "systems": {"make": {"extra": true}}} }, + { "name": "numeric option", "valid": false, "document": {"version": 1, "systems": {"make": {"options": {"threads": 2}}}} }, + { "name": "null option", "valid": false, "document": {"version": 1, "systems": {"make": {"options": {"profiles": null}}}} }, + { "name": "mixed list", "valid": false, "document": {"version": 1, "systems": {"make": {"options": {"profiles": ["ci", false]}}}} }, { "name": "empty system id", "valid": false, "document": {"version": 1, "systems": {"": {}}} }, - { "name": "invalid system id", "valid": false, "document": {"version": 1, "systems": {"maven.plugin": {}}} }, - { "name": "invalid option id", "valid": false, "document": {"version": 1, "systems": {"maven": {"options": {"profile/value": []}}}} }, - { "name": "root escape", "valid": false, "document": {"version": 1, "systems": {"maven": {"instances": {"../outside": {}}}}} }, - { "name": "absolute root", "valid": false, "document": {"version": 1, "systems": {"maven": {"instances": {"/outside": {}}}}} }, - { "name": "windows root", "valid": false, "document": {"version": 1, "systems": {"maven": {"instances": {"C:/outside": {}}}}} }, - { "name": "empty segment", "valid": false, "document": {"version": 1, "systems": {"maven": {"instances": {"a//b": {}}}}} }, - { "name": "backslash root", "valid": false, "document": {"version": 1, "systems": {"maven": {"instances": {"a\\b": {}}}}} }, - { "name": "malformed saved choices", "valid": false, "document": {"version": 1, "systems": {"maven": {"savedChoices": {"profiles": [false]}}}} } + { "name": "invalid system id", "valid": false, "document": {"version": 1, "systems": {"make.plugin": {}}} }, + { "name": "invalid option id", "valid": false, "document": {"version": 1, "systems": {"make": {"options": {"profile/value": []}}}} }, + { "name": "root escape", "valid": false, "document": {"version": 1, "systems": {"make": {"instances": {"../outside": {}}}}} }, + { "name": "absolute root", "valid": false, "document": {"version": 1, "systems": {"make": {"instances": {"/outside": {}}}}} }, + { "name": "windows root", "valid": false, "document": {"version": 1, "systems": {"make": {"instances": {"C:/outside": {}}}}} }, + { "name": "empty segment", "valid": false, "document": {"version": 1, "systems": {"make": {"instances": {"a//b": {}}}}} }, + { "name": "backslash root", "valid": false, "document": {"version": 1, "systems": {"make": {"instances": {"a\\b": {}}}}} }, + { "name": "malformed saved choices", "valid": false, "document": {"version": 1, "systems": {"make": {"savedChoices": {"profiles": [false]}}}} } ] } diff --git a/rust/fixtures/debug/hot-code-replace-v1.json b/rust/fixtures/debug/hot-code-replace-v1.json index fa36df2d..3931dff2 100644 --- a/rust/fixtures/debug/hot-code-replace-v1.json +++ b/rust/fixtures/debug/hot-code-replace-v1.json @@ -3,7 +3,7 @@ "cases": [ {"name":"replacement", "body":{"changedClasses":["b.B","a.A","b.B"]}, "result":{"kind":"redefineClasses","changedClasses":["a.A","b.B"]}}, {"name":"no changes", "body":{"changedClasses":[]}, "result":{"kind":"redefineClasses","changedClasses":[]}}, - {"name":"DAP success with JVM failure", "body":{"changedClasses":[],"errorMessage":"Schema change not supported"}, "failure":true}, + {"name":"DAP success with a runtime failure", "body":{"changedClasses":[],"errorMessage":"Schema change not supported"}, "failure":true}, {"name":"missing result", "body":{}, "failure":true}, {"name":"invalid class name", "body":{"changedClasses":[3]}, "failure":true} ] diff --git a/rust/fixtures/editor/basic-file-highlighting-v1.json b/rust/fixtures/editor/basic-file-highlighting-v1.json deleted file mode 100644 index ebd121ee..00000000 --- a/rust/fixtures/editor/basic-file-highlighting-v1.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "version": 1, - "associations": [ - { "filename": "Makefile", "language": "makefile" }, - { "filename": "makefile", "language": "makefile" }, - { "filename": "GNUmakefile", "language": "makefile" }, - { "filename": "Makefile.local", "language": "makefile" }, - { "filename": "rules.mk", "language": "makefile" }, - { "filename": "rules.MAK", "language": "makefile" }, - { "filename": "CMakeLists.txt", "language": "cmake" }, - { "filename": "toolchain.cmake", "language": "cmake" }, - { "filename": "script.groovy", "language": "groovy" }, - { "filename": "build.gradle", "language": "groovy" }, - { "filename": "Jenkinsfile", "language": "groovy" }, - { "filename": "build.gradle.kts", "language": "kotlin" }, - { "filename": "deploy.ps1", "language": "powershell" }, - { "filename": "module.psm1", "language": "powershell" }, - { "filename": "module.psd1", "language": "powershell" }, - { "filename": "build.bat", "language": "batch" }, - { "filename": "build.cmd", "language": "batch" }, - { "filename": "setup.zsh", "language": "bash" }, - { "filename": ".bashrc", "language": "bash" }, - { "filename": ".zshrc", "language": "bash" }, - { "filename": ".bash_profile", "language": "bash" }, - { "filename": ".zprofile", "language": "bash" }, - { "filename": ".profile", "language": "bash" }, - { "filename": ".env", "language": "dotenv" }, - { "filename": ".env.production", "language": "dotenv" }, - { "filename": "Containerfile", "language": "dockerfile" }, - { "filename": "Dockerfile.dev", "language": "dockerfile" }, - { "filename": "Containerfile.dev", "language": "dockerfile" } - ], - "syntax": [ - { "filename": "Makefile", "language": "makefile", "text": "# build\nSOURCES := $(wildcard src/*.c)\nall: $(SOURCES)\n\t@echo $@", "tokens": [{ "line": 1, "text": "#", "role": "comment" }, { "line": 2, "text": "wildcard", "role": "function" }] }, - { "filename": "CMakeLists.txt", "language": "cmake", "text": "# configure\nproject(Example)\nset(NAME \"example\")", "tokens": [{ "line": 1, "text": "#", "role": "comment" }, { "line": 2, "text": "project", "role": "function" }, { "line": 3, "text": "example", "role": "string" }] }, - { "filename": "build.gradle", "language": "groovy", "text": "// build\ndef name = \"example\"\nplugins { id 'java' }", "tokens": [{ "line": 1, "text": "//", "role": "comment" }, { "line": 2, "text": "def", "role": "keyword" }, { "line": 2, "text": "example", "role": "string" }] }, - { "filename": "deploy.ps1", "language": "powershell", "text": "# deploy\nWrite-Host \"example\"", "tokens": [{ "line": 1, "text": "#", "role": "comment" }, { "line": 2, "text": "example", "role": "string" }] }, - { "filename": "build.cmd", "language": "batch", "text": "REM build\necho \"example\"", "tokens": [{ "line": 1, "text": "REM", "role": "comment" }] } - ] -} diff --git a/rust/fixtures/execution/framework-identity.json b/rust/fixtures/execution/framework-identity.json deleted file mode 100644 index 15d0cf05..00000000 --- a/rust/fixtures/execution/framework-identity.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "name": "Framework identity and service classification for detected configurations", - "notes": [ - "A detected configuration carries the framework name in its provider extension so a client can label the service without re-reading the manifest.", - "Frameworks are matched most-specific-first, because a meta-framework declares the framework it is built on: a Nuxt app also depends on Vue.", - "A script is a service when its body names a program that serves, not when its name looks like `dev` or `start`. A name-only guess is reported as `heuristic` so a detector holding a real declaration wins deduplication.", - "Deduplication identity is (cwd, lowercased name); the provider is excluded, so the same service seen by two detectors merges to the higher confidence." - ], - "requests": [ - { - "command": "runConfig.generate", - "payload": { "root": "/workspace" }, - "workspace": { - "web/package.json": "{\"dependencies\":{\"nuxt\":\"^3.11.0\",\"vue\":\"^3.4.0\"},\"scripts\":{\"dev\":\"nuxt dev\",\"build\":\"nuxt build\"}}", - "site/package.json": "{\"dependencies\":{\"next\":\"^14.2.0\",\"react\":\"^18.3.0\"},\"scripts\":{\"dev\":\"next dev\"}}", - "admin/package.json": "{\"dependencies\":{\"vue\":\"^3.4.0\"},\"devDependencies\":{\"vite\":\"^5.0.0\"},\"scripts\":{\"dev\":\"vite\",\"check\":\"tsc -p .\"}}" - }, - "expected": { - "configurations": [ - { - "id": "npm.script:web/dev", - "execution": "service", - "confidence": "declared", - "extensions": { "npm": { "framework": "nuxt", "server": "nuxt" } } - }, - { - "id": "npm.script:web/build", - "execution": "task", - "extensions": { "npm": { "framework": "nuxt" } } - }, - { - "id": "npm.script:site/dev", - "execution": "service", - "extensions": { "npm": { "framework": "next", "server": "next" } } - }, - { - "id": "npm.script:admin/dev", - "execution": "service", - "extensions": { "npm": { "framework": "vue", "server": "vite" } } - }, - { - "id": "npm.script:admin/check", - "execution": "task", - "extensions": { "npm": { "framework": "vue" } } - } - ] - } - }, - { - "command": "runConfig.generate", - "payload": { "root": "/workspace" }, - "workspace": { - "package.json": "{\"scripts\":{\"start\":\"tsc -p .\",\"local\":\"NODE_ENV=development npx vite --port 3000\",\"legacy\":\"./scripts/dev.sh\"}}" - }, - "expected": { - "configurations": [ - { - "id": "npm.script:start", - "execution": "task", - "reason": "The body names a program that runs to completion, so the conventional `start` name is not trusted." - }, - { - "id": "npm.script:local", - "execution": "service", - "confidence": "declared", - "reason": "Environment assignments and `npx` wrap the program without changing it, and bare `vite` serves." - }, - { - "id": "npm.script:legacy", - "execution": "task", - "reason": "An unrecognised body and a name that is not conventionally a server." - } - ] - } - }, - { - "command": "runConfig.generate", - "payload": { "root": "/workspace" }, - "workspace": { - "declared/pyproject.toml": "[project]\nname = \"declared\"\ndependencies = [\"fastapi>=0.110\", \"uvicorn[standard]\"]\n", - "declared/main.py": "from fastapi import FastAPI\napp = FastAPI()\n", - "guessed/main.py": "from fastapi import FastAPI\napp = FastAPI()\n", - "edge/asgi.py": "from starlette.applications import Starlette\napp = Starlette()\n" - }, - "expected": { - "configurations": [ - { - "id": "python.uvicorn:declared/main", - "execution": "service", - "confidence": "declared", - "command": "uvicorn", - "args": ["main:app", "--reload"], - "extensions": { - "python": { "module": "main", "framework": "fastapi" } - } - }, - { - "id": "python.uvicorn:guessed/main", - "confidence": "heuristic", - "reason": "No dependency list, so the framework import in one file is the only evidence." - }, - { - "id": "python.uvicorn:edge/asgi", - "command": "uvicorn", - "args": ["asgi:app", "--reload"], - "extensions": { - "python": { "module": "asgi", "framework": "starlette" } - } - } - ] - } - } - ] -} diff --git a/rust/fixtures/execution/gradle.json b/rust/fixtures/execution/gradle.json deleted file mode 100644 index 28a3e365..00000000 --- a/rust/fixtures/execution/gradle.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "name": "Gradle service detection and toolchain requirements", - "notes": [ - "A Gradle build is runnable when a plugin contributes a start task. The build script is matched for plugin ids rather than executed, because running `gradle tasks` on project open would spawn a daemon per module.", - "Subproject tasks are invoked from the build root, because Gradle only resolves task paths from the directory holding the settings file. The configuration name is the full Gradle project path so two modules with the same leaf name stay distinct.", - "The command is the bare program name `gradle`. Wrapper preference travels in the toolchain requirement so the platform adapter resolves `./gradlew` or `gradlew.bat` for its own operating system." - ], - "requests": [ - { - "command": "runConfig.generate", - "payload": { "root": "/workspace" }, - "workspace": { - "build.gradle": "plugins { id 'org.springframework.boot' version '3.2.0' }\n", - "gradlew": "#!/bin/sh\n" - }, - "expected": { - "configurations": [ - { - "id": "gradle.service:bootRun", - "provider": "gradle.service", - "name": "bootRun", - "execution": "service", - "confidence": "declared", - "command": "gradle", - "args": ["bootRun"], - "cwd": ".", - "source": "build.gradle", - "extensions": { - "gradle": { - "task": "bootRun", - "plugin": "org.springframework.boot", - "project": ":" - } - } - } - ], - "toolchains": { - "project-gradle": { - "type": "gradle", - "wrapper": "./gradlew", - "java": "project-jdk" - }, - "project-jdk": { "type": "java" } - } - } - }, - { - "command": "runConfig.generate", - "payload": { "root": "/workspace" }, - "workspace": { - "settings.gradle": "include 'services:orders'\ninclude 'services:shipping'\n", - "services/orders/build.gradle": "plugins { id 'org.springframework.boot' }\n", - "services/shipping/build.gradle": "plugins { id 'application' }\n" - }, - "expected": { - "configurations": [ - { - "id": "gradle.service:services:orders", - "provider": "gradle.service", - "name": "services:orders", - "execution": "service", - "command": "gradle", - "args": [":services:orders:bootRun"], - "cwd": ".", - "source": "build.gradle" - }, - { - "id": "gradle.application:services:shipping", - "provider": "gradle.application", - "name": "services:shipping", - "execution": "application", - "command": "gradle", - "args": [":services:shipping:run"], - "cwd": ".", - "source": "build.gradle" - } - ] - } - }, - { - "command": "runConfig.generate", - "payload": { "root": "/workspace" }, - "workspace": { - "build.gradle": "plugins { id 'java-library' }\n// id 'org.springframework.boot'\n" - }, - "expected": { - "configurations": [], - "reason": "A library build contributes no start task, and a commented plugin id is not applied." - } - } - ] -} diff --git a/rust/fixtures/execution/maven-java-main-source-sets-v1.json b/rust/fixtures/execution/maven-java-main-source-sets-v1.json deleted file mode 100644 index e7781682..00000000 --- a/rust/fixtures/execution/maven-java-main-source-sets-v1.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "version": 1, - "name": "Maven Java main source-set launch plans", - "cases": [ - { - "name": "test source main", - "module": "module-a", - "source": "module-a/src/test/java/com/example/App.java", - "sourceSet": "test", - "configurationId": "java-main:com.example.App:module-a", - "mainClass": "com.example.App", - "classPaths": ["/workspace/module-a/target/test-classes"], - "modulePaths": [], - "expectedArguments": ["com.example.App"] - }, - { - "name": "main source main", - "module": "module-b", - "source": "module-b/src/main/java/com/example/App.java", - "sourceSet": "main", - "configurationId": "java-main:com.example.App:module-b", - "mainClass": "module-b/com.example.App", - "classPaths": ["/workspace/module-b/target/classes"], - "modulePaths": ["/workspace/module-b/target/modules"], - "expectedArguments": ["-m", "module-b/com.example.App"] - } - ] -} diff --git a/rust/fixtures/execution/maven-runtime-versions.json b/rust/fixtures/execution/maven-runtime-versions.json deleted file mode 100644 index 96166679..00000000 --- a/rust/fixtures/execution/maven-runtime-versions.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "description": "Maven and mvnd expose the embedded Apache Maven version, never the daemon banner.", - "cases": [ - { "output": "Apache Maven 3.9.11 (fixture)\nMaven home: /fixture/maven\n", "version": "3.9.11" }, - { "output": "Apache Maven Daemon (mvnd) 1.0.6 darwin-aarch64\nApache Maven 3.9.11 (fixture)\n", "version": "3.9.11" }, - { "output": "Apache Maven Daemon (mvnd) 1.0.6 windows-amd64\r\nApache Maven 4.0.0-rc-3 (fixture)\r\n", "version": "4.0.0-rc-3" }, - { "output": "Apache Maven Daemon (mvnd) 1.0.6\n", "version": null }, - { "output": "Java home is invalid\n", "version": null } - ] -} diff --git a/rust/fixtures/execution/maven.json b/rust/fixtures/execution/maven.json deleted file mode 100644 index a8e2a993..00000000 --- a/rust/fixtures/execution/maven.json +++ /dev/null @@ -1,245 +0,0 @@ -{ - "name": "Maven framework service detection from the declared module graph", - "notes": [ - "A plugin is what makes a goal available, so the plugin a module applies -- not a `@SpringBootApplication` annotation in a source file -- decides both that a module is a service and which framework it is. An annotation judge reads only the files the caller passed, so the same project generated a different document on every call and every team or local override joined by id came loose.", - "Each framework gets its own provider (`spring-boot.maven`, `quarkus.maven`, `micronaut.maven`) because the goals invented different property names for the same three things. Spring Boot takes `-Dspring-boot.run.jvmArguments` / `.arguments` / `.main-class` and `spring-boot:run`; Quarkus takes `-Djvm.args` / `-Dquarkus.args` and `quarkus:dev`; Micronaut takes `-Dmn.jvmArgs` / `-Dmn.appArgs` and `mn:run`. Passing one framework's names to another's goal is silently ignored, and the service starts with none of the user's arguments.", - "Only Spring Boot's goal accepts a main class. Quarkus and Micronaut resolve it from the build -- the Micronaut mojo's `mainClass` has no user property at all -- so the annotation scan grafts a class onto `spring-boot.maven` configurations only.", - "Debugging is per framework too. `spring-boot:run` forwards JVM arguments verbatim, so JDWP travels among them. Quarkus (`-Ddebug= -Dsuspend=y`) and Micronaut (`-Dmn.debug=true -Dmn.debug.port= -Dmn.debug.suspend=true`) start the agent themselves, so JDWP must NOT also be injected into their JVM arguments: two agents on one port fail to bind and the service never starts. Both are told to suspend because a breakpoint in initialisation is otherwise unreachable -- neither suspends by default.", - "Maven modules are declared, not discovered: the detector reads the graph from the root `pom.xml` instead of judging each directory the shared walk visits. A directory-driven scan is wrong in both directions -- `` may name a directory the walk prunes (one called `build` or `out` is invisible), while a leftover `pom.xml` outside the graph is not part of the build at all.", - "A caller only needs to supply workspace-relative Java paths. Maven root discovery checks their ancestor directories, so a reactor nested below the opened workspace is still selected even when the platform does not include `pom.xml` in the request.", - "Only `` counts. A plugin under `` pins a version for children without applying it, and one under `` never runs. `pom` packaging is an aggregator: it produces no artifact to run, so a plugin declared there configures its children rather than describing a service.", - "Maven is reached through the project toolchain binding rather than a bare program name, because the build may be driven by `./mvnw` instead of an installed `mvn`. Every configuration runs from the reactor root and addresses its module with `-pl`, so `cwd` stays `.` and the module travels in `extensions.maven.module`. The name is therefore what keeps two modules apart: `artifactId` where unique, the module path where two modules share one.", - "The annotation scan still supplies the main class, which is the only place it is named. Where a Spring Boot module holds exactly one annotated class, it becomes `extensions.maven.mainClass`; two candidates are ambiguous and are left for `spring-boot:run` to resolve. A class in a module with no framework plugin is still a runnable `java.main` entry." - ], - "requests": [ - { - "command": "runConfig.generate", - "payload": { "root": "/workspace", "paths": ["backend/src/main/java/com/demo/DemoApplication.java"] }, - "workspace": { - "pom.xml": "platformpombackendshared-lib", - "backend/pom.xml": "backendorg.springframework.bootspring-boot-maven-plugin", - "shared-lib/pom.xml": "shared-lib", - "backend/src/main/java/com/demo/DemoApplication.java": "package com.demo;\n@SpringBootApplication\npublic class DemoApplication { public static void main(String[] a) {} }\n" - }, - "expected": { - "configurations": [ - { - "id": "spring-boot.maven:backend", - "provider": "spring-boot.maven", - "name": "backend", - "execution": "service", - "confidence": "declared", - "command": null, - "cwd": ".", - "source": "backend/pom.xml", - "toolchains": { "java": "project-jdk", "maven": "project-maven" }, - "debug": { "adapter": "jdwp" }, - "extensions": { - "maven": { "module": "backend", "mainClass": "com.demo.DemoApplication" } - } - } - ], - "absentIds": ["spring-boot.maven:shared-lib", "spring:com.demo.DemoApplication"], - "reason": "The library module applies no boot plugin. The annotated class contributes its name to the module's service rather than a service of its own." - } - }, - { - "command": "runConfig.generate", - "payload": { "root": "/workspace", "paths": ["projects/demo/backend/src/main/java/com/demo/DemoApplication.java", "samples/Standalone.java"] }, - "workspace": { - "projects/demo/pom.xml": "platformpombackend", - "projects/demo/backend/pom.xml": "backendorg.springframework.bootspring-boot-maven-plugin", - "projects/demo/backend/src/main/java/com/demo/DemoApplication.java": "package com.demo;\n@SpringBootApplication\npublic class DemoApplication { public static void main(String[] a) {} }\n", - "samples/Standalone.java": "public class Standalone { public static void main(String[] a) {} }\n" - }, - "expected": { - "configurations": [ - { - "id": "spring-boot.maven:backend", - "provider": "spring-boot.maven", - "cwd": "projects/demo", - "source": "projects/demo/backend/pom.xml", - "toolchains": { "java": "project-jdk", "maven": "project-maven" }, - "extensions": { "maven": { "module": "backend", "mainClass": "com.demo.DemoApplication" } } - }, - { - "id": "java-main:Standalone", - "provider": "java.main", - "cwd": ".", - "source": "samples/Standalone.java", - "toolchains": { "java": "project-jdk" }, - "extensions": { "maven": { "module": ".", "mainClass": "Standalone" }, "java": { "source": "samples/Standalone.java" } } - } - ], - "reason": "The Maven Java source discovers and binds to the nested reactor, while the standalone source in the same request remains a direct JDK launch." - } - }, - { - "command": "runConfig.generate", - "payload": { "root": "/workspace" }, - "workspace": { - "pom.xml": "rootpombuild", - "build/pom.xml": "builder-serviceorg.springframework.bootspring-boot-maven-plugin" - }, - "expected": { - "configurations": [ - { - "id": "spring-boot.maven:builder-service", - "provider": "spring-boot.maven", - "execution": "service", - "source": "build/pom.xml", - "extensions": { "maven": { "module": "build" } } - } - ], - "reason": "The shared walk prunes directories named `build`, but `` names this one explicitly, so reading the declared graph is what makes it visible." - } - }, - { - "command": "runConfig.generate", - "payload": { "root": "/workspace" }, - "workspace": { - "pom.xml": "parentpomservice-aorg.springframework.bootspring-boot-maven-plugin", - "service-a/pom.xml": "service-aorg.springframework.bootspring-boot-maven-plugin" - }, - "expected": { - "configurations": [ - { - "id": "spring-boot.maven:service-a", - "provider": "spring-boot.maven", - "execution": "service", - "extensions": { "maven": { "module": "service-a" } } - } - ], - "absentIds": ["spring-boot.maven:parent"], - "reason": "The aggregator produces no artifact, so the plugin it declares configures its children." - } - }, - { - "command": "runConfig.generate", - "payload": { "root": "/workspace" }, - "workspace": { - "pom.xml": "rootpomteam-a/gatewayteam-b/gateway", - "team-a/gateway/pom.xml": "gatewayorg.springframework.bootspring-boot-maven-plugin", - "team-b/gateway/pom.xml": "gatewayorg.springframework.bootspring-boot-maven-plugin" - }, - "expected": { - "configurations": [ - { - "id": "spring-boot.maven:team-a/gateway", - "name": "team-a/gateway", - "extensions": { "maven": { "module": "team-a/gateway" } } - }, - { - "id": "spring-boot.maven:team-b/gateway", - "name": "team-b/gateway", - "extensions": { "maven": { "module": "team-b/gateway" } } - } - ], - "reason": "`artifactId` is only unique per `groupId`. Both modules share one and all Spring Boot configurations share `cwd`, so the name falls back to the module path or one of two real services would be dropped during dedup." - } - }, - { - "command": "runConfig.generate", - "payload": { "root": "/workspace" }, - "workspace": { - "pom.xml": "liborg.springframework.bootspring-boot-maven-plugin3.2.0" - }, - "expected": { - "configurations": [], - "reason": "`pluginManagement` pins a version for children without applying the plugin, so nothing here is runnable." - } - }, - { - "command": "runConfig.generate", - "payload": { "root": "/workspace" }, - "workspace": { - "pom.xml": "platformpomboot-apiquarkus-apimicronaut-apiquarkus-platform-apimicronaut-maven-api", - "boot-api/pom.xml": "boot-apiorg.springframework.bootspring-boot-maven-plugin", - "quarkus-api/pom.xml": "quarkus-apiio.quarkusquarkus-maven-plugin", - "micronaut-api/pom.xml": "micronaut-apiio.micronaut.buildmicronaut-maven-plugin", - "quarkus-platform-api/pom.xml": "quarkus-platform-apiio.quarkus.platformquarkus-maven-plugin", - "micronaut-maven-api/pom.xml": "micronaut-maven-apiio.micronaut.mavenmicronaut-maven-plugin" - }, - "expected": { - "configurations": [ - { - "id": "spring-boot.maven:boot-api", - "provider": "spring-boot.maven", - "execution": "service", - "extensions": { "maven": { "module": "boot-api" } } - }, - { - "id": "quarkus.maven:quarkus-api", - "provider": "quarkus.maven", - "execution": "service", - "extensions": { "maven": { "module": "quarkus-api" } } - }, - { - "id": "micronaut.maven:micronaut-api", - "provider": "micronaut.maven", - "execution": "service", - "extensions": { "maven": { "module": "micronaut-api" } } - }, - { - "id": "quarkus.maven:quarkus-platform-api", - "provider": "quarkus.maven", - "execution": "service", - "extensions": { "maven": { "module": "quarkus-platform-api" } } - }, - { - "id": "micronaut.maven:micronaut-maven-api", - "provider": "micronaut.maven", - "execution": "service", - "extensions": { "maven": { "module": "micronaut-maven-api" } } - } - ], - "reason": "The plugin decides the framework, and the framework decides the provider, because each goal takes its arguments under different property names." - } - }, - { - "command": "runConfig.createLaunchPlan", - "payload": { "root": "/workspace", "configurationId": "quarkus.maven:api", "debugPort": 5005 }, - "notes": "Given `extensions.maven` of `{module: api, jvmArguments: [-Xmx2g], programArguments: [--dev]}`.", - "expected": { - "executable": { "toolchain": "project-maven" }, - "workingDirectory": ".", - "arguments": [ - "-B", - "-ntp", - "-pl", - "api", - "-am", - "-Djvm.args=-Xmx2g", - "-Dquarkus.args=--dev", - "-Ddebug=5005", - "-Dsuspend=y", - "quarkus:dev" - ], - "reason": "`quarkus:dev` starts the debug agent itself given a port, so JDWP is absent from `jvm.args` -- two agents on one port would fail to bind." - } - }, - { - "command": "runConfig.createLaunchPlan", - "payload": { "root": "/workspace", "configurationId": "micronaut.maven:api", "debugPort": 5005 }, - "notes": "Given the same `extensions.maven` as the Quarkus case.", - "expected": { - "executable": { "toolchain": "project-maven" }, - "workingDirectory": ".", - "arguments": [ - "-B", - "-ntp", - "-pl", - "api", - "-am", - "-Dmn.jvmArgs=-Xmx2g", - "-Dmn.appArgs=--dev", - "-Dmn.debug=true", - "-Dmn.debug.port=5005", - "-Dmn.debug.suspend=true", - "mn:run" - ], - "reason": "The Micronaut run mojo takes debugging as three separate properties, and no main class: it resolves one from the build." - } - } - ] -} diff --git a/rust/fixtures/execution/standalone-java-compile-run-v1.json b/rust/fixtures/execution/standalone-java-compile-run-v1.json deleted file mode 100644 index 94455756..00000000 --- a/rust/fixtures/execution/standalone-java-compile-run-v1.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "version": 1, - "name": "Standalone Java compile-then-run launch plans", - "description": "Non-Maven java.main sources compile with javac into a per-configuration cache directory, then launch by qualified class name so JDK 8 (which lacks the JEP 330 single-file source launcher) can run them.", - "outputDirectoryPrefix": ".lithe/run/classes/", - "cases": [ - { - "name": "default package", - "source": "samples/Standalone.java", - "package": null, - "configurationId": "java-main:Standalone", - "outputDirectory": ".lithe/run/classes/java-main-Standalone", - "expectedMainClass": "Standalone", - "expectedClasspath": [".lithe/run/classes/java-main-Standalone"], - "expectedPreLaunchSteps": [ - { - "executable": { "toolchain": "project-jdk", "tool": "javac" }, - "arguments": ["-d", ".lithe/run/classes/java-main-Standalone", "samples/Standalone.java"] - } - ], - "expectedArguments": ["Standalone"] - }, - { - "name": "package qualified", - "source": "src/com/example/WorkerMain.java", - "package": "com.example", - "configurationId": "java-main:com.example.WorkerMain", - "outputDirectory": ".lithe/run/classes/java-main-com.example.WorkerMain", - "expectedMainClass": "com.example.WorkerMain", - "expectedClasspath": [".lithe/run/classes/java-main-com.example.WorkerMain"], - "expectedPreLaunchSteps": [ - { - "executable": { "toolchain": "project-jdk", "tool": "javac" }, - "arguments": ["-d", ".lithe/run/classes/java-main-com.example.WorkerMain", "src/com/example/WorkerMain.java"] - } - ], - "expectedArguments": ["com.example.WorkerMain"] - } - ] -} diff --git a/rust/fixtures/ide-api/calls.json b/rust/fixtures/ide-api/calls.json index 44ccd6cd..98d0f499 100644 --- a/rust/fixtures/ide-api/calls.json +++ b/rust/fixtures/ide-api/calls.json @@ -5,17 +5,10 @@ "name": "lithe_project_inspect", "arguments": {} }, - { - "name": "lithe_environment_configure", - "arguments": { - "javaHomePath": "", - "mavenExecutablePath": "mvnw" - } - }, { "name": "lithe_build_execute", "arguments": { - "instanceID": "maven@.", + "instanceID": "fixture@.", "taskID": "custom", "projectID": "services/api", "userArguments": [ @@ -43,39 +36,32 @@ { "name": "lithe_build_reload", "arguments": { - "instanceID": "maven@." - } - }, - { - "name": "lithe_maven_execute", - "arguments": { - "goals": [ - "clean", - "verify" - ], - "modulePath": "services/api" + "instanceID": "fixture@." } } ], "rejected": [ { - "name": "lithe_environment_configure", + "name": "lithe_build_configure", "arguments": { + "instanceID": "fixture@.", + "optionID": "token", + "value": "never-return-a-secret", "apiKey": "never-return-a-secret" } }, { "name": "lithe_build_execute", "arguments": { - "instanceID": "maven@.", + "instanceID": "fixture@.", "taskID": "verify", - "command": "mvn" + "command": "fixture-tool" } }, { "name": "lithe_build_execute", "arguments": { - "instanceID": "maven@.", + "instanceID": "fixture@.", "taskID": "verify", "userArguments": [ 42 @@ -93,23 +79,6 @@ "arguments": { "sessionID": "ambiguous-output-slot" } - }, - { - "name": "lithe_maven_execute", - "arguments": { - "goals": [ - "verify" - ], - "modulePath": "../outside" - } - }, - { - "name": "lithe_maven_execute", - "arguments": { - "goals": [ - "verify && echo unexpected" - ] - } } ] } diff --git a/rust/fixtures/ide-api/host-tool-exposure.json b/rust/fixtures/ide-api/host-tool-exposure.json index be938ef0..39fc5442 100644 --- a/rust/fixtures/ide-api/host-tool-exposure.json +++ b/rust/fixtures/ide-api/host-tool-exposure.json @@ -39,12 +39,6 @@ { "name": "lithe_project_inspect" }, - { - "name": "lithe_environment_inspect" - }, - { - "name": "lithe_environment_configure" - }, { "name": "lithe_run_list" }, @@ -63,18 +57,6 @@ { "name": "lithe_operation_stop" }, - { - "name": "lithe_maven_inspect" - }, - { - "name": "lithe_maven_configure" - }, - { - "name": "lithe_maven_reload" - }, - { - "name": "lithe_maven_execute" - }, { "name": "lithe_build_inspect" }, diff --git a/rust/fixtures/java/basic.json b/rust/fixtures/java/basic.json deleted file mode 100644 index 989b30b8..00000000 --- a/rust/fixtures/java/basic.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "Java structure", - "requests": [ - { - "command": "java.structure", - "payload": { - "source": "import a.A;\nimport b.B;\ninterface Service { String call(String value); }\n" - }, - "expected": { - "foldKind": "imports", - "syntaxRoles": [ - "functionDeclaration", - "keyword", - "parameter", - "punctuation", - "type" - ] - } - } - ] -} diff --git a/rust/fixtures/java/run-markers-v1.json b/rust/fixtures/java/run-markers-v1.json deleted file mode 100644 index 46795031..00000000 --- a/rust/fixtures/java/run-markers-v1.json +++ /dev/null @@ -1,216 +0,0 @@ -{ - "version": 1, - "cases": [ - { - "name": "main-methods-including-nested-class", - "request": { - "mainMethods": [ - { - "mainClass": "demo.App", - "projectName": "app", - "range": { "startLine": 3, "startUtf16Column": 23, "endLine": 3, "endUtf16Column": 27 } - }, - { - "mainClass": "demo.App$Inner", - "projectName": "app", - "range": { "startLine": 6, "startUtf16Column": 27, "endLine": 6, "endUtf16Column": 31 } - } - ] - }, - "expected": { - "markers": [ - { "line": 3, "endLine": 3, "kind": "main", "label": "App.main()", "mainClass": "demo.App", "projectName": "app", "status": "none" }, - { "line": 6, "endLine": 6, "kind": "main", "label": "Inner.main()", "mainClass": "demo.App$Inner", "projectName": "app", "status": "none" } - ] - } - }, - { - "name": "nested-test-classes-with-recorded-outcomes", - "request": { - "testItems": [ - { - "id": "app@demo.OrderTest", - "label": "OrderTest", - "fullName": "demo.OrderTest", - "projectName": "app", - "testKind": 0, - "testLevel": 5, - "range": { "startLine": 5, "startUtf16Column": 6, "endLine": 14, "endUtf16Column": 1 }, - "children": [ - { - "id": "app@demo.OrderTest#creates()", - "label": "creates()", - "fullName": "demo.OrderTest#creates()", - "projectName": "app", - "testKind": 0, - "testLevel": 6, - "range": { "startLine": 7, "startUtf16Column": 9, "endLine": 7, "endUtf16Column": 21 }, - "children": [] - }, - { - "id": "app@demo.OrderTest$Refunds", - "label": "Refunds", - "fullName": "demo.OrderTest$Refunds", - "projectName": "app", - "testKind": 0, - "testLevel": 5, - "range": { "startLine": 10, "startUtf16Column": 10, "endLine": 13, "endUtf16Column": 5 }, - "children": [ - { - "id": "app@demo.OrderTest$Refunds#refunds()", - "label": "refunds()", - "fullName": "demo.OrderTest$Refunds#refunds()", - "projectName": "app", - "testKind": 0, - "testLevel": 6, - "range": { "startLine": 12, "startUtf16Column": 13, "endLine": 12, "endUtf16Column": 25 }, - "children": [] - } - ] - } - ] - } - ], - "testCases": [ - { "className": "demo.OrderTest", "method": "creates", "status": "passed", "message": null, "invocations": 1 }, - { "className": "demo.OrderTest$Refunds", "method": "refunds", "status": "failed", "message": "expected refund", "invocations": 1 } - ] - }, - "expected": { - "markers": [ - { "line": 5, "endLine": 14, "kind": "testClass", "label": "OrderTest", "testClass": "demo.OrderTest", "testItemId": "app@demo.OrderTest", "status": "failed" }, - { "line": 7, "endLine": 7, "kind": "testMethod", "label": "OrderTest.creates", "testClass": "demo.OrderTest", "testMethod": "creates", "testItemId": "app@demo.OrderTest#creates()", "status": "passed" }, - { "line": 10, "endLine": 13, "kind": "testClass", "label": "Refunds", "testClass": "demo.OrderTest$Refunds", "testItemId": "app@demo.OrderTest$Refunds", "status": "failed" }, - { "line": 12, "endLine": 12, "kind": "testMethod", "label": "Refunds.refunds", "testClass": "demo.OrderTest$Refunds", "testMethod": "refunds", "testItemId": "app@demo.OrderTest$Refunds#refunds()", "status": "failed" } - ] - } - }, - { - "name": "nested-test-classes-with-canonical-provider-outcomes", - "request": { - "testItems": [ - { - "id": "app@demo.OrderTest", - "label": "OrderTest", - "fullName": "demo.OrderTest", - "projectName": "app", - "testKind": 0, - "testLevel": 5, - "range": { - "startLine": 5, - "startUtf16Column": 6, - "endLine": 14, - "endUtf16Column": 1 - }, - "children": [ - { - "id": "app@demo.OrderTest#creates()", - "label": "creates()", - "fullName": "demo.OrderTest#creates()", - "projectName": "app", - "testKind": 0, - "testLevel": 6, - "range": { - "startLine": 7, - "startUtf16Column": 9, - "endLine": 7, - "endUtf16Column": 21 - }, - "children": [] - }, - { - "id": "app@demo.OrderTest$Refunds", - "label": "Refunds", - "fullName": "demo.OrderTest$Refunds", - "projectName": "app", - "testKind": 0, - "testLevel": 5, - "range": { - "startLine": 10, - "startUtf16Column": 10, - "endLine": 13, - "endUtf16Column": 5 - }, - "children": [ - { - "id": "app@demo.OrderTest$Refunds#refunds()", - "label": "refunds()", - "fullName": "demo.OrderTest$Refunds#refunds()", - "projectName": "app", - "testKind": 0, - "testLevel": 6, - "range": { - "startLine": 12, - "startUtf16Column": 13, - "endLine": 12, - "endUtf16Column": 25 - }, - "children": [] - } - ] - } - ] - } - ], - "testCases": [ - { - "status": "passed", - "message": null, - "invocations": 1, - "suite": "demo.OrderTest", - "case": "creates" - }, - { - "status": "failed", - "message": "expected refund", - "invocations": 1, - "suite": "demo.OrderTest$Refunds", - "case": "refunds" - } - ] - }, - "expected": { - "markers": [ - { - "line": 5, - "endLine": 14, - "kind": "testClass", - "label": "OrderTest", - "testClass": "demo.OrderTest", - "testItemId": "app@demo.OrderTest", - "status": "failed" - }, - { - "line": 7, - "endLine": 7, - "kind": "testMethod", - "label": "OrderTest.creates", - "testClass": "demo.OrderTest", - "testMethod": "creates", - "testItemId": "app@demo.OrderTest#creates()", - "status": "passed" - }, - { - "line": 10, - "endLine": 13, - "kind": "testClass", - "label": "Refunds", - "testClass": "demo.OrderTest$Refunds", - "testItemId": "app@demo.OrderTest$Refunds", - "status": "failed" - }, - { - "line": 12, - "endLine": 12, - "kind": "testMethod", - "label": "Refunds.refunds", - "testClass": "demo.OrderTest$Refunds", - "testMethod": "refunds", - "testItemId": "app@demo.OrderTest$Refunds#refunds()", - "status": "failed" - } - ] - } - } - ] -} diff --git a/rust/fixtures/lsp/java-build-report-v1.json b/rust/fixtures/lsp/java-build-report-v1.json deleted file mode 100644 index 94b69b11..00000000 --- a/rust/fixtures/lsp/java-build-report-v1.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "version": 1, - "continuableFailures": [ - { - "code": "javaBuildCompilationErrors", - "message": "The Java project build reported compilation errors.", - "javaBuildReport": { - "markerScope": "launchTarget", - "builderFailedEarlier": false, - "elapsedMilliseconds": 506, - "recovery": "rebuildJavaIndex" - } - }, - { - "code": "javaBuildFailed", - "message": "The Java language service could not complete the project build. Check the Java language server log for the build error.", - "javaBuildReport": { - "markerScope": "workspace", - "builderFailedEarlier": true, - "elapsedMilliseconds": 5012, - "recovery": "rebuildJavaIndex" - } - } - ], - "nonContinuableCodes": [ - "invalidServerResult", - "javaBuildCancelled", - "requestCancelled", - "requestTimeout" - ] -} diff --git a/rust/fixtures/lsp/java-completion-v1.json b/rust/fixtures/lsp/java-completion-v1.json deleted file mode 100644 index 224648fe..00000000 --- a/rust/fixtures/lsp/java-completion-v1.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "version": 1, - "pluginCompletion": { - "guessMethodArguments": "insertParameterNames", - "favoriteStaticMembers": ["java.util.Objects.*"], - "filteredTypes": ["example.internal.*"] - }, - "javaSettings": { - "completion": { - "guessMethodArguments": "off", - "favoriteStaticMembers": ["java.util.Objects.*"], - "filteredTypes": ["example.internal.*"] - } - }, - "workspaceConfigurationSections": [ - "java", - "java.completion", - "java.completion.guessMethodArguments", - "java.completion.favoriteStaticMembers", - "java.completion.filteredTypes", - "java.completion.unknown" - ] -} diff --git a/rust/fixtures/lsp/java-launch-preparation-v1.json b/rust/fixtures/lsp/java-launch-preparation-v1.json deleted file mode 100644 index 59f3e91f..00000000 --- a/rust/fixtures/lsp/java-launch-preparation-v1.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "version": 1, - "cases": [ - { - "id": "windows-module-and-case", - "rootUri": "file:///C:/Work", - "sourcePath": "c:/work/APP/Main.java", - "mainClass": "demo.Main", - "entries": [ - { - "mainClass": "demo.Main", - "projectName": "lib", - "filePath": "C:/Work/lib/Main.java" - }, - { - "mainClass": "demo.app/demo.Main", - "projectName": "app", - "filePath": "C:/Work/app/Main.java" - }, - { - "mainClass": "demo.Other", - "projectName": "app", - "filePath": "C:/Work/app/Main.java" - } - ], - "paths": [ - [ - "C:/Work/custom/modules" - ], - [ - "C:/Work/custom/tests", - "C:/cache/test-only.jar" - ] - ], - "target": { - "mainClass": "demo.app/demo.Main", - "projectName": "app", - "modulePaths": [ - "C:/Work/custom/modules" - ], - "classPaths": [ - "C:/Work/custom/tests", - "C:/cache/test-only.jar" - ] - } - }, - { - "id": "mac-source-identity", - "rootUri": "file:///workspace", - "sourcePath": "/workspace/app/Main.java", - "mainClass": "demo.Main", - "entries": [ - { - "mainClass": "demo.Main", - "projectName": "lib", - "filePath": "/workspace/lib/Main.java" - }, - { - "mainClass": "demo.Main", - "projectName": "app", - "filePath": "/workspace/app/Main.java" - } - ], - "paths": [ - [], - [ - "/workspace/app/custom-output" - ] - ], - "target": { - "mainClass": "demo.Main", - "projectName": "app", - "modulePaths": [], - "classPaths": [ - "/workspace/app/custom-output" - ] - } - } - ] -} diff --git a/rust/fixtures/lsp/java-navigation-v1.json b/rust/fixtures/lsp/java-navigation-v1.json deleted file mode 100644 index f339e8bb..00000000 --- a/rust/fixtures/lsp/java-navigation-v1.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "version": 1, - "cases": [ - { - "name": "four-marker-variants-use-deterministic-ordering", - "request": { - "sessionId": "session-1", - "uri": "file:///workspace/src/Service.java", - "documentVersion": 7 - }, - "expectedResult": { - "documentVersion": 7, - "markers": [ - { - "line": 2, - "utf16Column": 9, - "implementationCount": 1, - "direction": "up", - "relation": "interface" - }, - { - "line": 2, - "utf16Column": 9, - "implementationCount": 2, - "direction": "down", - "relation": "interface" - }, - { - "line": 8, - "utf16Column": 16, - "implementationCount": 1, - "direction": "up", - "relation": "inheritance" - }, - { - "line": 8, - "utf16Column": 16, - "implementationCount": 3, - "direction": "down", - "relation": "inheritance" - } - ] - } - }, - { - "name": "zero-target-declarations-are-omitted", - "request": { - "sessionId": "session-1", - "uri": "file:///workspace/src/Unused.java", - "documentVersion": 3 - }, - "expectedResult": { - "documentVersion": 3, - "markers": [] - } - } - ] -} diff --git a/rust/fixtures/lsp/java-project-runtimes-v1.json b/rust/fixtures/lsp/java-project-runtimes-v1.json deleted file mode 100644 index 10e1a51b..00000000 --- a/rust/fixtures/lsp/java-project-runtimes-v1.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "version": 1, - "cases": [ - { - "name": "selected-custom-major-wins", - "candidates": [ - { - "homePath": "/bundled/jdk21", - "version": "21" - }, - { - "homePath": "/jdks/jdk25", - "version": "25" - }, - { - "homePath": "/custom/jdk25", - "version": "25.0.1", - "isDefault": true - }, - { - "homePath": "/jdks/jdk17", - "version": "17", - "isDefault": true - } - ], - "expected": [ - { - "name": "JavaSE-17", - "path": "/jdks/jdk17" - }, - { - "name": "JavaSE-21", - "path": "/bundled/jdk21" - }, - { - "name": "JavaSE-25", - "path": "/custom/jdk25", - "default": true - } - ] - }, - { - "name": "legacy-without-default", - "candidates": [ - { - "homePath": "/jdks/jdk21", - "version": "21" - } - ], - "expected": [ - { - "name": "JavaSE-21", - "path": "/jdks/jdk21" - } - ] - }, - { - "name": "invalid-default-is-ignored", - "candidates": [ - { - "homePath": "", - "version": "25", - "isDefault": true - }, - { - "homePath": "/jdks/jdk21", - "version": "21" - } - ], - "expected": [ - { - "name": "JavaSE-21", - "path": "/jdks/jdk21" - } - ] - } - ] -} diff --git a/rust/fixtures/lsp/java-workspace-policy-v1.json b/rust/fixtures/lsp/java-workspace-policy-v1.json deleted file mode 100644 index 8154b9fe..00000000 --- a/rust/fixtures/lsp/java-workspace-policy-v1.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "version": 1, - "cases": [ - { - "name": "mixed-workspace", - "workspacePaths": [ - "README.md", - "node_modules/example/Fake.java", - "src/Main.java", - "target/generated/Generated.java" - ], - "changedPaths": [ - "README.md", - "build.gradle.kts", - "src/Main.java", - "target/generated/Generated.java" - ], - "expected": { - "shouldStart": true, - "representativeJavaPath": "src/Main.java", - "changes": [ - { "path": "README.md", "kind": "other" }, - { "path": "build.gradle.kts", "kind": "buildConfiguration" }, - { "path": "src/Main.java", "kind": "source" }, - { "path": "target/generated/Generated.java", "kind": "ignored" } - ] - } - }, - { - "name": "ignored-java-only", - "workspacePaths": [ - ".worktrees/topic/src/Topic.java", - "build/generated/Generated.java", - "node_modules/example/Fake.java" - ], - "changedPaths": [ - ".mvn/wrapper/maven-wrapper.properties", - "gradle/wrapper/gradle-wrapper.properties", - "settings.gradle" - ], - "expected": { - "shouldStart": false, - "changes": [ - { "path": ".mvn/wrapper/maven-wrapper.properties", "kind": "buildConfiguration" }, - { "path": "gradle/wrapper/gradle-wrapper.properties", "kind": "buildConfiguration" }, - { "path": "settings.gradle", "kind": "buildConfiguration" } - ] - } - }, - { - "name": "maven-project-with-conventional-layout", - "workspacePaths": [ - "pom.xml", - "src/main/java/com/example/demo/DemoApplication.java" - ], - "changedPaths": [], - "expected": { - "shouldStart": true, - "representativeJavaPath": "src/main/java/com/example/demo/DemoApplication.java", - "changes": [] - } - }, - { - "name": "java-fixture-inside-another-ecosystem", - "workspacePaths": [ - "Cargo.toml", - "Package.swift", - "rust/fixtures/projects/demo/pom.xml", - "rust/fixtures/projects/demo/src/main/java/com/example/demo/DemoApplication.java" - ], - "changedPaths": [], - "expected": { - "shouldStart": false, - "representativeJavaPath": "rust/fixtures/projects/demo/src/main/java/com/example/demo/DemoApplication.java", - "changes": [] - } - } - ] -} diff --git a/rust/fixtures/lsp/jdt-cache-retention-v1.json b/rust/fixtures/lsp/jdt-cache-retention-v1.json deleted file mode 100644 index 98f4b776..00000000 --- a/rust/fixtures/lsp/jdt-cache-retention-v1.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "version": 1, - "cases": [ - { - "name": "expires-only-inactive-cache-older-than-thirty-days", - "nowUnixSeconds": 4000000, - "activeWorkspaceKey": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "entries": [ - { - "workspaceKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "lastModifiedUnixSeconds": 1000000 - }, - { - "workspaceKey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "lastModifiedUnixSeconds": 1408000 - }, - { - "workspaceKey": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "lastModifiedUnixSeconds": 1 - }, - { - "workspaceKey": "not-a-workspace-key", - "lastModifiedUnixSeconds": 1 - } - ], - "expected": { - "retentionDays": 30, - "expiredWorkspaceKeys": [ - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - ] - } - }, - { - "name": "duplicate-observations-use-newest-timestamp", - "nowUnixSeconds": 4000000, - "entries": [ - { - "workspaceKey": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", - "lastModifiedUnixSeconds": 1 - }, - { - "workspaceKey": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", - "lastModifiedUnixSeconds": 3999999 - } - ], - "expected": { - "retentionDays": 30, - "expiredWorkspaceKeys": [] - } - } - ] -} diff --git a/rust/fixtures/lsp/jdt-direct-launch-v1.json b/rust/fixtures/lsp/jdt-direct-launch-v1.json deleted file mode 100644 index 90959256..00000000 --- a/rust/fixtures/lsp/jdt-direct-launch-v1.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "schemaVersion": 1, - "request": { - "providerId": "java", - "executablePath": "/opt/lithe/jdtls/bin/jdtls", - "arguments": [ - "--jvm-arg=-Duser.language=en" - ], - "environment": { - "JAVA_HOME": "/opt/lithe/jdk" - }, - "rootUri": "file:///workspace/project", - "workingDirectory": "/workspace/project", - "initializationOptions": null, - "runtimeExecutablePath": "/opt/lithe/jdk/bin/java", - "jdtlsLaunchResources": { - "launcherJarPath": "/opt/lithe/jdtls/plugins/org.eclipse.equinox.launcher_1.7.0.jar", - "configurationDirectory": "/opt/lithe/jdtls/config_mac", - "lombokAgentPath": "/opt/lithe/jdtls/lombok/lombok.jar", - "javaDebugBundlePath": "/opt/lithe/jdtls/java-debug/com.microsoft.java.debug.plugin-0.53.1.jar", - "javaExtensionBundlePaths": [ - "/opt/lithe/jdtls/java-debug/com.microsoft.java.debug.plugin-0.53.1.jar", - "/opt/lithe/jdtls/java-test/extensions/com.microsoft.java.test.plugin-0.42.0.jar" - ] - }, - "cacheDirectory": "/var/cache/lithe/language-servers", - "workspaceFingerprint": "build=|modules=|jdtls=1.55.0", - "initializeTimeoutMilliseconds": 30000, - "serviceReadyIdleTimeoutMilliseconds": 45000, - "serviceReadyAbsoluteTimeoutMilliseconds": 600000, - "requestTimeoutMilliseconds": 30000, - "shutdownTimeoutMilliseconds": 2000 - } -} diff --git a/rust/fixtures/lsp/jdt-readiness-v1.json b/rust/fixtures/lsp/jdt-readiness-v1.json deleted file mode 100644 index 2d1172b4..00000000 --- a/rust/fixtures/lsp/jdt-readiness-v1.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "schemaVersion": 1, - "idleWarningMilliseconds": 45000, - "absoluteTimeoutMilliseconds": 600000, - "cases": [ - { - "name": "quiet Maven import eventually becomes ready", - "steps": [ - { - "atMilliseconds": 0, - "action": "progress", - "percentage": 23 - }, - { - "atMilliseconds": 46000, - "action": "poll", - "warning": true, - "timeout": false - }, - { - "atMilliseconds": 60000, - "action": "poll", - "warning": false, - "timeout": false - }, - { - "atMilliseconds": 90000, - "action": "ready" - }, - { - "atMilliseconds": 601000, - "action": "poll", - "warning": false, - "timeout": false - } - ] - }, - { - "name": "quiet Maven import reaches the absolute deadline", - "steps": [ - { - "atMilliseconds": 46000, - "action": "poll", - "warning": true, - "timeout": false - }, - { - "atMilliseconds": 600000, - "action": "poll", - "warning": false, - "timeout": true, - "classification": "noProgressStall" - } - ] - }, - { - "name": "progress never extends the absolute deadline", - "steps": [ - { - "atMilliseconds": 599000, - "action": "progress", - "percentage": 99 - }, - { - "atMilliseconds": 600000, - "action": "poll", - "warning": false, - "timeout": true, - "classification": "projectImportActive" - } - ] - } - ] -} diff --git a/rust/fixtures/lsp/jdt-workspace-fingerprint-v1.json b/rust/fixtures/lsp/jdt-workspace-fingerprint-v1.json deleted file mode 100644 index 2e0103d8..00000000 --- a/rust/fixtures/lsp/jdt-workspace-fingerprint-v1.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "version": 1, - "cases": [ - { - "name": "sorts-and-deduplicates-platform-observations", - "buildFiles": [ - { "path": "pom.xml" }, - { "path": "build.gradle.kts" }, - { "path": "pom.xml" } - ], - "directMavenModules": ["core", "api", "core"], - "jdtlsVersion": "1.38.0", - "workspaceFingerprint": "build=build.gradle.kts,pom.xml|modules=api,core|jdtls=1.38.0" - }, - { - "name": "gradle-wrapper-version-selects-the-state", - "buildFiles": [ - { "path": "settings.gradle.kts" }, - { "path": "build.gradle.kts" }, - { "path": "gradle/wrapper/gradle-wrapper.properties" } - ], - "directMavenModules": [], - "gradleWrapperDistribution": "https://services.gradle.org/distributions/gradle-8.10-bin.zip", - "jdtlsVersion": "1.38.0", - "workspaceFingerprint": "build=build.gradle.kts,gradle/wrapper/gradle-wrapper.properties,settings.gradle.kts|modules=|jdtls=1.38.0|gradle=https://services.gradle.org/distributions/gradle-8.10-bin.zip" - }, - { - "name": "version-only-workspace", - "buildFiles": [], - "directMavenModules": [], - "jdtlsVersion": "7.6.5", - "workspaceFingerprint": "build=|modules=|jdtls=7.6.5" - } - ] -} diff --git a/rust/fixtures/lsp/jdt-workspace-key-v1.json b/rust/fixtures/lsp/jdt-workspace-key-v1.json deleted file mode 100644 index 16655c8e..00000000 --- a/rust/fixtures/lsp/jdt-workspace-key-v1.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": 1, - "cases": [ - { - "name": "legacy-workspace-path", - "workspaceRoot": "/workspace/demo", - "workspaceFingerprint": null, - "workspaceKey": "4de1d2f8b60db00ae8307a6fe19690b23893974b259297719107bb84e767148a" - }, - { - "name": "maven-modules-and-jdtls-version", - "workspaceRoot": "/workspace/demo", - "workspaceFingerprint": "build=pom.xml@1700000000000:42|modules=api,core|jdtls=1.38.0", - "workspaceKey": "b21826804a57043eec9f155c7ee324870f7df48d7d51a311e25cb04e9671df25" - } - ] -} diff --git a/rust/fixtures/lsp/maven-profile-events-v1.json b/rust/fixtures/lsp/maven-profile-events-v1.json deleted file mode 100644 index ed53dd6e..00000000 --- a/rust/fixtures/lsp/maven-profile-events-v1.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": 1, - "events": [ - { "type": "log", "sequence": 1, "providerId": "java", "sessionId": "java-session", "mavenProfileTask": "running" }, - { "type": "log", "sequence": 2, "providerId": "java", "sessionId": "java-session", "mavenProfileProject": { "projectUri": "file:///common-11111111", "status": "running" } }, - { "type": "stateChanged", "sequence": 3, "providerId": "java", "sessionId": "java-session", "state": "ready" }, - { "type": "log", "sequence": 4, "providerId": "java", "sessionId": "java-session", "mavenProfileProject": { "projectUri": "file:///common-11111111", "status": "failed", "errorDetails": "Maven profile project update failed." } }, - { "type": "log", "sequence": 5, "providerId": "java", "sessionId": "java-session", "mavenProfileProject": { "projectUri": "file:///common-22222222", "status": "succeeded" } }, - { "type": "log", "sequence": 6, "providerId": "java", "sessionId": "java-session", "mavenProfileTask": "partiallySucceeded" } - ] -} diff --git a/rust/fixtures/lsp/project-preparation-v1.json b/rust/fixtures/lsp/project-preparation-v1.json deleted file mode 100644 index eb84cdf6..00000000 --- a/rust/fixtures/lsp/project-preparation-v1.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": 1, - "snapshots": [ - { "phase": "starting", "status": "loading", "blocksRun": true }, - { "phase": "importing", "status": "loading", "blocksRun": true }, - { "phase": "configuring", "status": "loading", "blocksRun": true }, - { "phase": "building", "status": "loading", "blocksRun": true }, - { "phase": "ready", "status": "ready", "blocksRun": false }, - { "phase": "configuring", "status": "failed", "blocksRun": false }, - { "phase": "stopped", "status": "idle", "blocksRun": true } - ] -} diff --git a/rust/fixtures/maven/basic.json b/rust/fixtures/maven/basic.json deleted file mode 100644 index 113eab02..00000000 --- a/rust/fixtures/maven/basic.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "recursive Maven project scan", - "request": { - "command": "maven.scan", - "payload": { - "root": "/workspace", - "paths": ["pom.xml", "module-a/pom.xml", "module-a/module-b/pom.xml"] - } - }, - "expected": { - "relativePath": ".", - "artifactId": "demo", - "packaging": "pom", - "sourceRoots": [], - "modules": [ - { - "relativePath": "module-a", - "sourceRoots": [ - { "path": "src/main/java", "kind": "mainJava" }, - { "path": "src/main/resources", "kind": "mainResources" }, - { "path": "src/test/java", "kind": "testJava" }, - { "path": "src/test/resources", "kind": "testResources" }, - { "path": "target/generated-sources", "kind": "generatedMain" }, - { "path": "target/generated-test-sources", "kind": "generatedTest" } - ], - "modules": [ - { - "relativePath": "module-a/module-b", - "sourceRoots": [ - { "path": "src/main/java", "kind": "mainJava" }, - { "path": "src/main/resources", "kind": "mainResources" }, - { "path": "src/test/java", "kind": "testJava" }, - { "path": "src/test/resources", "kind": "testResources" }, - { "path": "target/generated-sources", "kind": "generatedMain" }, - { "path": "target/generated-test-sources", "kind": "generatedTest" } - ] - } - ] - } - ], - "profiles": [ - { - "id": "dev", - "isActiveByDefault": true - } - ], - "hasWrapper": true - } -} diff --git a/rust/fixtures/maven/dependency-tree-v2.json b/rust/fixtures/maven/dependency-tree-v2.json deleted file mode 100644 index de3937b1..00000000 --- a/rust/fixtures/maven/dependency-tree-v2.json +++ /dev/null @@ -1,140 +0,0 @@ -{ - "version": 2, - "modulePath": "service", - "treeFile": "com.example:service:jar:1.0.0\n+- org.example:runtime-lib:jar:2.0:runtime\n| \\- org.example:transitive-lib:jar:1.5:runtime (scope updated from test)\n+- org.example:provided-lib:jar:3.0:provided (scope managed from compile)\n+- org.example:test-lib:jar:tests:4.0:test\n+- org.example:managed-lib:jar:2.1:compile (version managed from 0.9.0; scope not updated to compile)\n\\- org.example:compile-lib:jar:1.0:compile\n +- (org.example:duplicate-lib:jar:1.0:compile - version managed from 1.0; omitted for duplicate)\n \\- (org.example:conflict-lib:jar:1.0:compile - version managed from 1.1; omitted for conflict with 2.0)\n", - "expected": { - "modulePath": "service", - "dependencies": [ - { - "modulePath": "service", - "groupId": "org.example", - "artifactId": "compile-lib", - "version": "1.0", - "type": "jar", - "classifier": null, - "scope": "compile", - "resolution": "resolved", - "selectedVersion": null, - "premanagedVersion": null, - "premanagedScope": null, - "originalScope": null, - "ignoredScope": null, - "children": [ - { - "modulePath": "service", - "groupId": "org.example", - "artifactId": "conflict-lib", - "version": "1.0", - "type": "jar", - "classifier": null, - "scope": "compile", - "resolution": "omittedConflict", - "selectedVersion": "2.0", - "premanagedVersion": "1.1", - "premanagedScope": null, - "originalScope": null, - "ignoredScope": null, - "children": [] - }, - { - "modulePath": "service", - "groupId": "org.example", - "artifactId": "duplicate-lib", - "version": "1.0", - "type": "jar", - "classifier": null, - "scope": "compile", - "resolution": "omittedDuplicate", - "selectedVersion": null, - "premanagedVersion": "1.0", - "premanagedScope": null, - "originalScope": null, - "ignoredScope": null, - "children": [] - } - ] - }, - { - "modulePath": "service", - "groupId": "org.example", - "artifactId": "managed-lib", - "version": "2.1", - "type": "jar", - "classifier": null, - "scope": "compile", - "resolution": "resolved", - "selectedVersion": null, - "premanagedVersion": "0.9.0", - "premanagedScope": null, - "originalScope": null, - "ignoredScope": "compile", - "children": [] - }, - { - "modulePath": "service", - "groupId": "org.example", - "artifactId": "provided-lib", - "version": "3.0", - "type": "jar", - "classifier": null, - "scope": "provided", - "resolution": "resolved", - "selectedVersion": null, - "premanagedVersion": null, - "premanagedScope": "compile", - "originalScope": null, - "ignoredScope": null, - "children": [] - }, - { - "modulePath": "service", - "groupId": "org.example", - "artifactId": "runtime-lib", - "version": "2.0", - "type": "jar", - "classifier": null, - "scope": "runtime", - "resolution": "resolved", - "selectedVersion": null, - "premanagedVersion": null, - "premanagedScope": null, - "originalScope": null, - "ignoredScope": null, - "children": [ - { - "modulePath": "service", - "groupId": "org.example", - "artifactId": "transitive-lib", - "version": "1.5", - "type": "jar", - "classifier": null, - "scope": "runtime", - "resolution": "resolved", - "selectedVersion": null, - "premanagedVersion": null, - "premanagedScope": null, - "originalScope": "test", - "ignoredScope": null, - "children": [] - } - ] - }, - { - "modulePath": "service", - "groupId": "org.example", - "artifactId": "test-lib", - "version": "4.0", - "type": "jar", - "classifier": "tests", - "scope": "test", - "resolution": "resolved", - "selectedVersion": null, - "premanagedVersion": null, - "premanagedScope": null, - "originalScope": null, - "ignoredScope": null, - "children": [] - } - ] - } -} diff --git a/rust/fixtures/maven/diagnostics.json b/rust/fixtures/maven/diagnostics.json deleted file mode 100644 index 33df033f..00000000 --- a/rust/fixtures/maven/diagnostics.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "Maven compiler diagnostics", - "request": { - "command": "maven.diagnostics", - "payload": { - "root": "/workspace", - "output": "[ERROR] src/main/java/App.java:[12,4] cannot find symbol\n[WARNING] src/main/java/App.java:[4] unused import\n" - } - }, - "expected": { - "issues": [ - { - "path": "src/main/java/App.java", - "line": 12, - "column": 4, - "severity": "error", - "message": "cannot find symbol" - }, - { - "path": "src/main/java/App.java", - "line": 4, - "column": null, - "severity": "warning", - "message": "unused import" - } - ] - } -} diff --git a/rust/fixtures/maven/launch-plan-v1.json b/rust/fixtures/maven/launch-plan-v1.json deleted file mode 100644 index 8d95427e..00000000 --- a/rust/fixtures/maven/launch-plan-v1.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "version": 1, - "cases": [ - { - "name": "nested reactor module lifecycle with local settings", - "context": { - "version": 1, - "reactorPath": "projects/demo", - "profiles": ["qa", "dev", "dev"], - "settingsPath": "/Users/example/.m2/settings.xml", - "localRepositoryPath": "/Users/example/.m2/custom-repository", - "skipTests": true, - "mavenExecutablePath": "/opt/apache-maven/bin/mvn", - "javaHomePath": "/Library/Java/JavaVirtualMachines/example/Contents/Home" - }, - "module": "service-api", - "goals": ["verify"], - "expected": { - "version": 1, - "executable": { "toolchain": "project-maven" }, - "arguments": [ - "-B", - "-ntp", - "-P", - "dev,qa", - "-s", - "/Users/example/.m2/settings.xml", - "-Dmaven.repo.local=/Users/example/.m2/custom-repository", - "-pl", - "service-api", - "-am", - "-DskipTests", - "verify" - ], - "workingDirectory": "projects/demo", - "configurationFingerprint": "sha256:0e84a43d287c8e889ff15740114e8de0d719cbf3ca6b6ce3530b97e164770f4a" - } - }, - { - "name": "root reactor custom goal preserves maven config behavior", - "context": { - "version": 1, - "reactorPath": ".", - "profiles": [], - "skipTests": false - }, - "module": null, - "goals": ["spring-boot:run"], - "expected": { - "version": 1, - "executable": { "toolchain": "project-maven" }, - "arguments": ["-B", "-ntp", "spring-boot:run"], - "workingDirectory": ".", - "configurationFingerprint": "sha256:65a5de6ae2a3135ad844ed30ba8558865f97a6696654a83d3d1d8544cc63380f" - } - }, - { - "name": "custom goal keeps property and quiet arguments as argv", - "context": { - "version": 1, - "reactorPath": ".", - "profiles": [], - "skipTests": false - }, - "module": null, - "goals": [ - "help:evaluate", - "-Dexpression=fixture.config", - "-q", - "-DforceStdout" - ], - "expected": { - "version": 1, - "executable": { "toolchain": "project-maven" }, - "arguments": [ - "-B", - "-ntp", - "help:evaluate", - "-Dexpression=fixture.config", - "-q", - "-DforceStdout" - ], - "workingDirectory": ".", - "configurationFingerprint": "sha256:65a5de6ae2a3135ad844ed30ba8558865f97a6696654a83d3d1d8544cc63380f" - } - } - ] -} diff --git a/rust/fixtures/maven/platform-contract-v1.json b/rust/fixtures/maven/platform-contract-v1.json deleted file mode 100644 index afa7ca9b..00000000 --- a/rust/fixtures/maven/platform-contract-v1.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "version": 1, - "lifecyclePhases": [ - "clean", - "validate", - "compile", - "test", - "package", - "verify", - "install", - "site", - "deploy" - ], - "storageIdentityCases": [ - { - "name": "macOS preserves workspace path case", - "platform": "macos", - "workspacePath": "/Users/Example/Projects/Lithe", - "reactorPath": "services/api", - "expectedIdentity": "/Users/Example/Projects/Lithe\u0000services/api" - }, - { - "name": "Windows folds workspace case and normalizes reactor separators", - "platform": "windows", - "workspacePath": "C:\\Users\\Example\\Projects\\Lithe", - "reactorPath": "services\\api", - "expectedIdentity": "c:\\users\\example\\projects\\lithe\u0000services/api" - } - ] -} diff --git a/rust/fixtures/maven/source-roots-v1.json b/rust/fixtures/maven/source-roots-v1.json deleted file mode 100644 index ac76765b..00000000 --- a/rust/fixtures/maven/source-roots-v1.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "version": 1, - "kindOrder": [ - "mainJava", - "mainResources", - "testJava", - "testResources", - "generatedMain", - "generatedTest" - ], - "cases": [ - { - "name": "standard module", - "sourceRoots": [ - { "path": "src/main/java", "kind": "mainJava" }, - { "path": "src/main/resources", "kind": "mainResources" }, - { "path": "src/test/java", "kind": "testJava" }, - { "path": "src/test/resources", "kind": "testResources" }, - { "path": "target/generated-sources", "kind": "generatedMain" }, - { "path": "target/generated-test-sources", "kind": "generatedTest" } - ] - }, - { - "name": "configured module", - "sourceRoots": [ - { "path": "custom/java", "kind": "mainJava" }, - { "path": "custom/resources", "kind": "mainResources" }, - { "path": "custom/test-java", "kind": "testJava" }, - { "path": "custom/test-resources", "kind": "testResources" }, - { "path": "target/generated-sources/annotations", "kind": "generatedMain" }, - { "path": "target/generated-test-sources", "kind": "generatedTest" } - ] - }, - { - "name": "custom build directory", - "sourceRoots": [ - { "path": "src/main/java", "kind": "mainJava" }, - { "path": "src/main/resources", "kind": "mainResources" }, - { "path": "src/test/java", "kind": "testJava" }, - { "path": "src/test/resources", "kind": "testResources" }, - { "path": "build-output/generated-sources/annotations", "kind": "generatedMain" }, - { "path": "build-output/generated-test-sources/fixtures", "kind": "generatedTest" } - ] - }, - { - "name": "execution-scoped generated sources", - "sourceRoots": [ - { "path": "src/main/java", "kind": "mainJava" }, - { "path": "src/main/resources", "kind": "mainResources" }, - { "path": "src/test/java", "kind": "testJava" }, - { "path": "src/test/resources", "kind": "testResources" }, - { "path": "target/generated-sources/openapi", "kind": "generatedMain" }, - { "path": "target/generated-test-sources/fixtures", "kind": "generatedTest" } - ] - } - ] -} diff --git a/rust/fixtures/maven/test-results-v1.json b/rust/fixtures/maven/test-results-v1.json deleted file mode 100644 index cea6a710..00000000 --- a/rust/fixtures/maven/test-results-v1.json +++ /dev/null @@ -1,304 +0,0 @@ -{ - "version": 1, - "cases": [ - { - "name": "junit5-failure-and-error", - "sourceFiles": ["src/test/java/com/example/CalculatorTest.java"], - "output": "[ERROR] Failures:\n[ERROR] additionIsCorrect(com.example.CalculatorTest): expected <4> but was <5>\n[ERROR] at com.example.CalculatorTest.additionIsCorrect(CalculatorTest.java:42)\n[ERROR] Errors:\n[ERROR] contextLoads(com.example.CalculatorTest): application context failed\n[ERROR] at com.example.CalculatorTest.contextLoads(CalculatorTest.java:55)\n[ERROR] Tests run: 4, Failures: 1, Errors: 1, Skipped: 1\n", - "expected": { - "testsRun": 4, - "failures": 1, - "errors": 1, - "skipped": 1, - "passed": 1, - "success": false, - "failureDetails": [ - { - "name": "additionIsCorrect(com.example.CalculatorTest)", - "kind": "failure", - "message": "expected <4> but was <5>", - "path": "src/test/java/com/example/CalculatorTest.java", - "line": 42, - "column": null - }, - { - "name": "contextLoads(com.example.CalculatorTest)", - "kind": "error", - "message": "application context failed", - "path": "src/test/java/com/example/CalculatorTest.java", - "line": 55, - "column": null - } - ], - "testCases": [] - } - }, - { - "name": "junit4-pass", - "sourceFiles": [], - "output": "[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0\n", - "expected": { - "testsRun": 2, - "failures": 0, - "errors": 0, - "skipped": 0, - "passed": 2, - "success": true, - "failureDetails": [], - "testCases": [] - } - }, - { - "name": "surefire-3-detailed-failure-with-reactor-footer", - "sourceFiles": ["src/test/java/review/AppTest.java"], - "output": "[ERROR] review.AppTest.targetFailure -- Time elapsed: 0.023 s <<< FAILURE!\norg.opentest4j.AssertionFailedError: expected review failure\n at org.junit.jupiter.api.Assertions.fail(Assertions.java:138)\n at review.AppTest.targetFailure(AppTest.java:4)\n\n[INFO] Results:\n[ERROR] Failures:\n[ERROR] AppTest.targetFailure:4 expected review failure\n[INFO] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0\n[INFO] Reactor Summary for reactor 1.0-SNAPSHOT:\n[INFO] base ................................ SUCCESS\n[INFO] app ................................ FAILURE\n", - "expected": { - "testsRun": 1, - "failures": 1, - "errors": 0, - "skipped": 0, - "passed": 0, - "success": false, - "failureDetails": [ - { - "name": "review.AppTest.targetFailure", - "kind": "failure", - "message": "expected review failure", - "path": "src/test/java/review/AppTest.java", - "line": 4, - "column": null - } - ], - "testCases": [] - } - }, - { - "name": "surefire-3-parameterized-detailed-failure", - "sourceFiles": ["src/test/java/review/AppTest.java"], - "output": "[ERROR] review.AppTest.targetFailure(int)[1] -- Time elapsed: 0.023 s <<< FAILURE!\njava.lang.AssertionError: expected review failure\n at review.AppTest.targetFailure(AppTest.java:4)\n\n[INFO] Results:\n[ERROR] Failures:\n[ERROR] targetFailure(int)[1](review.AppTest):4 expected review failure\n[INFO] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0\n", - "expected": { - "testsRun": 1, - "failures": 1, - "errors": 0, - "skipped": 0, - "passed": 0, - "success": false, - "failureDetails": [ - { - "name": "review.AppTest.targetFailure(int)[1]", - "kind": "failure", - "message": "expected review failure", - "path": "src/test/java/review/AppTest.java", - "line": 4, - "column": null - } - ], - "testCases": [] - } - }, - { - "name": "surefire-2-detailed-failure", - "sourceFiles": ["src/test/java/review/AppTest.java"], - "output": "[ERROR] fails(review.AppTest) Time elapsed: 0.01 s <<< FAILURE!\njava.lang.AssertionError: expected review failure\n at review.AppTest.fails(AppTest.java:9)\n[INFO] Results:\n[ERROR] Failures:\n[ERROR] fails(review.AppTest): expected review failure\n[INFO] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0\n", - "expected": { - "testsRun": 1, - "failures": 1, - "errors": 0, - "skipped": 0, - "passed": 0, - "success": false, - "failureDetails": [ - { - "name": "fails(review.AppTest)", - "kind": "failure", - "message": "expected review failure", - "path": "src/test/java/review/AppTest.java", - "line": 9, - "column": null - } - ], - "testCases": [] - } - }, - { - "name": "qualified-source-wins-over-simple-name", - "sourceFiles": [ - "module-a/src/test/java/other/AppTest.java", - "module-b/src/test/java/review/AppTest.java" - ], - "output": "[ERROR] review.AppTest.fails -- Time elapsed: 0.01 s <<< FAILURE!\njava.lang.AssertionError: expected review failure\n at review.AppTest.fails(AppTest.java:12)\n[INFO] Results:\n[ERROR] Failures:\n[ERROR] AppTest.fails:12 expected review failure\n[INFO] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0\n", - "expected": { - "testsRun": 1, - "failures": 1, - "errors": 0, - "skipped": 0, - "passed": 0, - "success": false, - "failureDetails": [ - { - "name": "review.AppTest.fails", - "kind": "failure", - "message": "expected review failure", - "path": "module-b/src/test/java/review/AppTest.java", - "line": 12, - "column": null - } - ], - "testCases": [] - } - }, - { - "name": "ambiguous-qualified-source-has-no-location", - "sourceFiles": [ - "module-a/src/test/java/review/AppTest.java", - "module-b/src/test/java/review/AppTest.java" - ], - "output": "[ERROR] review.AppTest.fails -- Time elapsed: 0.01 s <<< FAILURE!\njava.lang.AssertionError: expected review failure\n at review.AppTest.fails(AppTest.java:12)\n[INFO] Results:\n[ERROR] Failures:\n[ERROR] AppTest.fails:12 expected review failure\n[INFO] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0\n", - "expected": { - "testsRun": 1, - "failures": 1, - "errors": 0, - "skipped": 0, - "passed": 0, - "success": false, - "failureDetails": [ - { - "name": "review.AppTest.fails", - "kind": "failure", - "message": "expected review failure", - "path": null, - "line": null, - "column": null - } - ], - "testCases": [] - } - }, - { - "name": "junit5-parameterized-nested-module", - "sourceFiles": ["service/src/test/java/com/example/CalculatorTest.java"], - "output": "[ERROR] Failures:\n[ERROR] additionIsCorrect(int)[1](com.example.CalculatorTest): expected <4> but was <5>\n[ERROR] at com.example.CalculatorTest.additionIsCorrect(CalculatorTest.java:42)\n[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0\n", - "expected": { - "testsRun": 1, - "failures": 1, - "errors": 0, - "skipped": 0, - "passed": 0, - "success": false, - "failureDetails": [ - { - "name": "additionIsCorrect(int)[1](com.example.CalculatorTest)", - "kind": "failure", - "message": "expected <4> but was <5>", - "path": "service/src/test/java/com/example/CalculatorTest.java", - "line": 42, - "column": null - } - ], - "testCases": [] - } - }, - { - "name": "packaged-source-wins-over-root-same-name", - "sourceFiles": [ - "src/test/java/AppTest.java", - "service/src/test/java/com/example/AppTest.java" - ], - "output": "[ERROR] com.example.AppTest.fails -- Time elapsed: 0.023 s <<< FAILURE!\njava.lang.AssertionError: expected failure\n at com.example.AppTest.fails(AppTest.java:8)\n\n[ERROR] Failures:\n[ERROR] com.example.AppTest.fails:8 expected failure\n[INFO] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0\n", - "expected": { - "testsRun": 1, - "failures": 1, - "errors": 0, - "skipped": 0, - "passed": 0, - "success": false, - "failureDetails": [ - { - "name": "com.example.AppTest.fails", - "kind": "failure", - "message": "expected failure", - "path": "service/src/test/java/com/example/AppTest.java", - "line": 8, - "column": null - } - ], - "testCases": [] - } - }, - { - "name": "duplicate-packaged-source-has-no-location", - "sourceFiles": [ - "service-one/src/test/java/com/example/AppTest.java", - "service-two/src/test/java/com/example/AppTest.java" - ], - "output": "[ERROR] com.example.AppTest.fails -- Time elapsed: 0.023 s <<< FAILURE!\njava.lang.AssertionError: expected failure\n at com.example.AppTest.fails(AppTest.java:8)\n\n[ERROR] Failures:\n[ERROR] com.example.AppTest.fails:8 expected failure\n[INFO] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0\n", - "expected": { - "testsRun": 1, - "failures": 1, - "errors": 0, - "skipped": 0, - "passed": 0, - "success": false, - "failureDetails": [ - { - "name": "com.example.AppTest.fails", - "kind": "failure", - "message": "expected failure", - "path": null, - "line": null, - "column": null - } - ], - "testCases": [] - } - }, - { - "name": "surefire-legacy-detailed-failure", - "sourceFiles": ["src/test/java/review/AppTest.java"], - "output": "review.AppTest.targetFailure Time elapsed: 0.01 sec <<< FAILURE!\njava.lang.AssertionError: expected legacy failure\n at org.junit.Assert.fail(Assert.java:88)\n at review.AppTest.targetFailure(AppTest.java:4)\n\nFailed tests:\n targetFailure(review.AppTest):4 expected legacy failure\n\nTests run: 1, Failures: 1, Errors: 0, Skipped: 0\n", - "expected": { - "testsRun": 1, - "failures": 1, - "errors": 0, - "skipped": 0, - "passed": 0, - "success": false, - "failureDetails": [ - { - "name": "review.AppTest.targetFailure", - "kind": "failure", - "message": "expected legacy failure", - "path": "src/test/java/review/AppTest.java", - "line": 4, - "column": null - } - ], - "testCases": [] - } - }, - { - "name": "junit4-failed-tests-footer-only", - "sourceFiles": ["src/test/java/review/AppTest.java"], - "output": "Failed tests:\n targetFailure(review.AppTest):4 expected legacy failure\n\nTests run: 1, Failures: 1, Errors: 0, Skipped: 0\n", - "expected": { - "testsRun": 1, - "failures": 1, - "errors": 0, - "skipped": 0, - "passed": 0, - "success": false, - "failureDetails": [ - { - "name": "targetFailure(review.AppTest)", - "kind": "failure", - "message": "expected legacy failure", - "path": "src/test/java/review/AppTest.java", - "line": 4, - "column": null - } - ], - "testCases": [] - } - } - ] -} diff --git a/rust/fixtures/maven/testcase-durations-v1.json b/rust/fixtures/maven/testcase-durations-v1.json deleted file mode 100644 index 114330b6..00000000 --- a/rust/fixtures/maven/testcase-durations-v1.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "xml": "\n\n\n\n\n\n\n\n\n\n\n\n", - "expected": [ - { - "className": "demo.DurationTest", - "method": "fast", - "status": "passed", - "message": null, - "invocations": 1, - "durationMillis": 10 - }, - { - "className": "demo.DurationTest", - "method": "invalid", - "status": "passed", - "message": null, - "invocations": 1 - }, - { - "className": "demo.DurationTest", - "method": "missing", - "status": "passed", - "message": null, - "invocations": 1 - }, - { - "className": "demo.DurationTest", - "method": "negative", - "status": "passed", - "message": null, - "invocations": 1 - }, - { - "className": "demo.DurationTest", - "method": "overflow", - "status": "passed", - "message": null, - "invocations": 1 - }, - { - "className": "demo.DurationTest", - "method": "partial", - "status": "passed", - "message": null, - "invocations": 2 - }, - { - "className": "demo.DurationTest", - "method": "repeat", - "status": "passed", - "message": null, - "invocations": 2, - "durationMillis": 50 - }, - { - "className": "demo.DurationTest", - "method": "slow", - "status": "passed", - "message": null, - "invocations": 1, - "durationMillis": 125 - }, - { - "className": "demo.DurationTest", - "method": "zero", - "status": "passed", - "message": null, - "invocations": 1, - "durationMillis": 0 - } - ] -} diff --git a/rust/fixtures/modules/built-in-v1.json b/rust/fixtures/modules/built-in-v1.json index aafa878d..d4a529e1 100644 --- a/rust/fixtures/modules/built-in-v1.json +++ b/rust/fixtures/modules/built-in-v1.json @@ -58,7 +58,6 @@ "dependencies": ["dev.lithe.workspace"], "capabilities": ["dev.lithe.capability.execution-workspace"], "contributions": [ - { "id": "execution.maven", "kind": "toolWindow", "actionID": "execution.maven.toggle", "rendererID": "execution.maven" }, { "id": "execution.run", "kind": "toolWindow", "actionID": "execution.run.toggle", "rendererID": "execution.run" }, { "id": "execution.tests", "kind": "toolWindow", "actionID": "execution.tests.toggle", "rendererID": "execution.tests" } ], diff --git a/rust/fixtures/mybatis/basic.json b/rust/fixtures/mybatis/basic.json deleted file mode 100644 index 76a40ce3..00000000 --- a/rust/fixtures/mybatis/basic.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "MyBatis mapper XML navigation index", - "request": { - "command": "mybatis.index", - "payload": { - "root": "/fixture/workspace", - "paths": [ - "src/main/java/example/UserMapper.java", - "src/main/resources/mapper/UserMapper.xml" - ], - "textOverrides": { - "src/main/resources/mapper/UserMapper.xml": "" - } - } - }, - "expected": { - "statement": { - "namespace": "example.UserMapper", - "statementId": "selectById", - "kind": "select", - "javaPath": "src/main/java/example/UserMapper.java", - "xmlPath": "src/main/resources/mapper/UserMapper.xml" - } - } -} diff --git a/rust/fixtures/projects/lithe-spring-boot-git-graph/.classpath b/rust/fixtures/projects/lithe-spring-boot-git-graph/.classpath deleted file mode 100644 index 4e7f6701..00000000 --- a/rust/fixtures/projects/lithe-spring-boot-git-graph/.classpath +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/rust/fixtures/projects/lithe-spring-boot-git-graph/.project b/rust/fixtures/projects/lithe-spring-boot-git-graph/.project deleted file mode 100644 index 441ab09e..00000000 --- a/rust/fixtures/projects/lithe-spring-boot-git-graph/.project +++ /dev/null @@ -1,34 +0,0 @@ - - - lithe-spring-boot-git-graph - - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.m2e.core.maven2Builder - - - - - - org.eclipse.jdt.core.javanature - org.eclipse.m2e.core.maven2Nature - - - - 1785591560813 - - 30 - - org.eclipse.core.resources.regexFilterMatcher - node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ - - - - diff --git a/rust/fixtures/projects/lithe-spring-boot-git-graph/.settings/org.eclipse.core.resources.prefs b/rust/fixtures/projects/lithe-spring-boot-git-graph/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index abdea9ac..00000000 --- a/rust/fixtures/projects/lithe-spring-boot-git-graph/.settings/org.eclipse.core.resources.prefs +++ /dev/null @@ -1,4 +0,0 @@ -eclipse.preferences.version=1 -encoding//src/main/java=UTF-8 -encoding//src/main/resources=UTF-8 -encoding/=UTF-8 diff --git a/rust/fixtures/projects/lithe-spring-boot-git-graph/.settings/org.eclipse.jdt.apt.core.prefs b/rust/fixtures/projects/lithe-spring-boot-git-graph/.settings/org.eclipse.jdt.apt.core.prefs deleted file mode 100644 index d4313d4b..00000000 --- a/rust/fixtures/projects/lithe-spring-boot-git-graph/.settings/org.eclipse.jdt.apt.core.prefs +++ /dev/null @@ -1,2 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.apt.aptEnabled=false diff --git a/rust/fixtures/projects/lithe-spring-boot-git-graph/.settings/org.eclipse.jdt.core.prefs b/rust/fixtures/projects/lithe-spring-boot-git-graph/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index e60fa3f3..00000000 --- a/rust/fixtures/projects/lithe-spring-boot-git-graph/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,10 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.methodParameters=generate -org.eclipse.jdt.core.compiler.codegen.targetPlatform=21 -org.eclipse.jdt.core.compiler.compliance=21 -org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled -org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore -org.eclipse.jdt.core.compiler.processAnnotations=disabled -org.eclipse.jdt.core.compiler.release=enabled -org.eclipse.jdt.core.compiler.source=21 diff --git a/rust/fixtures/projects/lithe-spring-boot-git-graph/.settings/org.eclipse.m2e.core.prefs b/rust/fixtures/projects/lithe-spring-boot-git-graph/.settings/org.eclipse.m2e.core.prefs deleted file mode 100644 index f897a7f1..00000000 --- a/rust/fixtures/projects/lithe-spring-boot-git-graph/.settings/org.eclipse.m2e.core.prefs +++ /dev/null @@ -1,4 +0,0 @@ -activeProfiles= -eclipse.preferences.version=1 -resolveWorkspaceProjects=true -version=1 diff --git a/rust/fixtures/projects/lithe-spring-boot-git-graph/README.md b/rust/fixtures/projects/lithe-spring-boot-git-graph/README.md deleted file mode 100644 index 284e6e94..00000000 --- a/rust/fixtures/projects/lithe-spring-boot-git-graph/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Lithe Spring Boot Git Graph Fixture - -这是 Git Graph 的单一验收项目,不参与 Lithe 本身的构建。 - -项目模拟一个常见的 Spring Boot 用户服务,配套脚本会生成: - -- `main` 主线 -- `feature/users` 分支及真实 Merge Commit -- `feature/orders` 的 Rebase 历史 -- Tag `v0.1.0` -- `origin/main` 远程引用 - -生成: - -```bash -./scripts/git-graph/create-git-graph-fixture.sh -``` diff --git a/rust/fixtures/projects/lithe-spring-boot-git-graph/history/OrderController-feature.java b/rust/fixtures/projects/lithe-spring-boot-git-graph/history/OrderController-feature.java deleted file mode 100644 index a5451a8a..00000000 --- a/rust/fixtures/projects/lithe-spring-boot-git-graph/history/OrderController-feature.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.demo.order; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api/orders") -public class OrderController { - @GetMapping("/health") - public String health() { - return "ok"; - } -} diff --git a/rust/fixtures/projects/lithe-spring-boot-git-graph/history/UserService-feature.java b/rust/fixtures/projects/lithe-spring-boot-git-graph/history/UserService-feature.java deleted file mode 100644 index cb649964..00000000 --- a/rust/fixtures/projects/lithe-spring-boot-git-graph/history/UserService-feature.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.demo.user; - -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class UserService { - private final UserRepository repository; - - public UserService(UserRepository repository) { - this.repository = repository; - } - - public List listUsers() { - return repository.findAll().stream().toList(); - } -} diff --git a/rust/fixtures/projects/lithe-spring-boot-git-graph/pom.xml b/rust/fixtures/projects/lithe-spring-boot-git-graph/pom.xml deleted file mode 100644 index 98da55eb..00000000 --- a/rust/fixtures/projects/lithe-spring-boot-git-graph/pom.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.3.5 - - - - com.example - lithe-spring-boot-git-graph - 0.1.0-SNAPSHOT - lithe-spring-boot-git-graph - - - 21 - - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-validation - - - org.springframework.boot - spring-boot-starter-test - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - diff --git a/rust/fixtures/projects/lithe-spring-boot-git-graph/src/main/java/com/example/demo/DemoApplication.java b/rust/fixtures/projects/lithe-spring-boot-git-graph/src/main/java/com/example/demo/DemoApplication.java deleted file mode 100644 index 1bc95549..00000000 --- a/rust/fixtures/projects/lithe-spring-boot-git-graph/src/main/java/com/example/demo/DemoApplication.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.demo; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class DemoApplication { - public static void main(String[] args) { - SpringApplication.run(DemoApplication.class, args); - } -} diff --git a/rust/fixtures/projects/lithe-spring-boot-git-graph/src/main/java/com/example/demo/user/UserController.java b/rust/fixtures/projects/lithe-spring-boot-git-graph/src/main/java/com/example/demo/user/UserController.java deleted file mode 100644 index abdefbac..00000000 --- a/rust/fixtures/projects/lithe-spring-boot-git-graph/src/main/java/com/example/demo/user/UserController.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.demo.user; - -import jakarta.validation.constraints.NotBlank; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; - -@Validated -@RestController -@RequestMapping("/api/users") -public class UserController { - private final UserService service; - - public UserController(UserService service) { - this.service = service; - } - - @GetMapping - public List list() { - return service.listUsers(); - } - - public record UserQuery(@NotBlank String tenant) {} -} diff --git a/rust/fixtures/projects/lithe-spring-boot-git-graph/src/main/java/com/example/demo/user/UserRepository.java b/rust/fixtures/projects/lithe-spring-boot-git-graph/src/main/java/com/example/demo/user/UserRepository.java deleted file mode 100644 index 2e1a0888..00000000 --- a/rust/fixtures/projects/lithe-spring-boot-git-graph/src/main/java/com/example/demo/user/UserRepository.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.demo.user; - -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public class UserRepository { - public List findAll() { - return List.of( - new UserSummary(1L, "Ada Lovelace"), - new UserSummary(2L, "Grace Hopper") - ); - } - - public record UserSummary(Long id, String name) {} -} diff --git a/rust/fixtures/projects/lithe-spring-boot-git-graph/src/main/java/com/example/demo/user/UserService.java b/rust/fixtures/projects/lithe-spring-boot-git-graph/src/main/java/com/example/demo/user/UserService.java deleted file mode 100644 index 2c2cf882..00000000 --- a/rust/fixtures/projects/lithe-spring-boot-git-graph/src/main/java/com/example/demo/user/UserService.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.demo.user; - -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class UserService { - private final UserRepository repository; - - public UserService(UserRepository repository) { - this.repository = repository; - } - - public List listUsers() { - return repository.findAll(); - } -} diff --git a/rust/fixtures/projects/lithe-spring-boot-git-graph/src/main/resources/application.yml b/rust/fixtures/projects/lithe-spring-boot-git-graph/src/main/resources/application.yml deleted file mode 100644 index 5befe7a5..00000000 --- a/rust/fixtures/projects/lithe-spring-boot-git-graph/src/main/resources/application.yml +++ /dev/null @@ -1,5 +0,0 @@ -spring: - application: - name: lithe-spring-boot-git-graph -server: - port: 8080 diff --git a/rust/fixtures/run-configuration/automatic-java-selection.json b/rust/fixtures/run-configuration/automatic-java-selection.json deleted file mode 100644 index 93c355ef..00000000 --- a/rust/fixtures/run-configuration/automatic-java-selection.json +++ /dev/null @@ -1,284 +0,0 @@ -{ - "version": 1, - "cases": [ - { - "name": "skip-incompatible-java-home", - "minimum": "17", - "candidates": [ - { - "id": "env", - "version": "1.8.0_402", - "priority": 0 - }, - { - "id": "path", - "version": "17.0.12", - "priority": 1 - }, - { - "id": "latest", - "version": "21.0.4", - "priority": 2 - } - ], - "fallbackId": "env" - }, - { - "name": "compatible-java-home-wins", - "minimum": "17", - "candidates": [ - { - "id": "env", - "version": "17.0.12", - "priority": 0 - }, - { - "id": "latest", - "version": "21.0.4", - "priority": 2 - } - ], - "fallbackId": "env" - }, - { - "name": "numeric-version-order", - "minimum": "8", - "candidates": [ - { - "id": "eleven", - "version": "11.0.22", - "priority": 2 - }, - { - "id": "twenty-one", - "version": "21.0.4", - "priority": 2 - }, - { - "id": "nine", - "version": "9.0.4", - "priority": 2 - } - ], - "fallbackId": "eleven" - }, - { - "name": "numeric-patch-order", - "minimum": "17", - "candidates": [ - { - "id": "nine", - "version": "17.0.9", - "priority": 2 - }, - { - "id": "ten", - "version": "17.0.10", - "priority": 2 - } - ], - "fallbackId": "nine" - }, - { - "name": "legacy-java-eight", - "minimum": "8", - "candidates": [ - { - "id": "env", - "version": "1.8.0_402", - "priority": 0 - }, - { - "id": "latest", - "version": "21.0.4", - "priority": 2 - } - ], - "fallbackId": "env" - }, - { - "name": "stable-tie", - "minimum": "17", - "candidates": [ - { - "id": "z", - "version": "17.0.12", - "priority": 2 - }, - { - "id": "a", - "version": "17.0.12", - "priority": 2 - } - ], - "fallbackId": "z" - }, - { - "name": "exact-feature-beats-newer-java-home", - "minimum": "8", - "candidates": [ - { - "id": "env", - "version": "21.0.4", - "priority": 0 - }, - { - "id": "zulu8", - "version": "1.8.0_504", - "priority": 2 - } - ], - "fallbackId": "env" - }, - { - "name": "oldest-newer-when-exact-missing", - "minimum": "11", - "candidates": [ - { - "id": "env", - "version": "21.0.4", - "priority": 0 - }, - { - "id": "seventeen", - "version": "17.0.12", - "priority": 2 - } - ], - "fallbackId": "env" - }, - { - "name": "priority-breaks-ties-within-feature", - "minimum": "17", - "candidates": [ - { - "id": "detected", - "version": "17.0.14", - "priority": 2 - }, - { - "id": "path", - "version": "17.0.12", - "priority": 1 - }, - { - "id": "latest", - "version": "21.0.4", - "priority": 0 - } - ], - "fallbackId": "latest" - }, - { - "name": "unsatisfied-retains-fallback", - "minimum": "21", - "candidates": [ - { - "id": "env", - "version": "8.0.402", - "priority": 0 - }, - { - "id": "other", - "version": "17.0.12", - "priority": 2 - } - ], - "fallbackId": "env" - }, - { - "name": "missing-requirement-retains-fallback", - "minimum": null, - "candidates": [ - { - "id": "env", - "version": "8.0.402", - "priority": 0 - }, - { - "id": "latest", - "version": "21.0.4", - "priority": 2 - } - ], - "fallbackId": "env" - }, - { - "name": "unknown-version-is-not-compatible", - "minimum": "17", - "candidates": [ - { - "id": "env", - "version": "", - "priority": 0 - }, - { - "id": "other", - "version": "17.0.12", - "priority": 2 - } - ], - "fallbackId": "env" - }, - { - "name": "no-runtimes", - "minimum": "17", - "candidates": [], - "fallbackId": null - } - ], - "expected": { - "skip-incompatible-java-home": { - "id": "path", - "warning": false - }, - "compatible-java-home-wins": { - "id": "env", - "warning": false - }, - "numeric-version-order": { - "id": "nine", - "warning": false - }, - "numeric-patch-order": { - "id": "ten", - "warning": false - }, - "legacy-java-eight": { - "id": "env", - "warning": false - }, - "stable-tie": { - "id": "a", - "warning": false - }, - "unsatisfied-retains-fallback": { - "id": "env", - "warning": true - }, - "missing-requirement-retains-fallback": { - "id": "env", - "warning": false - }, - "unknown-version-is-not-compatible": { - "id": "other", - "warning": false - }, - "no-runtimes": { - "id": null, - "warning": true - }, - "exact-feature-beats-newer-java-home": { - "id": "zulu8", - "warning": false - }, - "oldest-newer-when-exact-missing": { - "id": "seventeen", - "warning": false - }, - "priority-breaks-ties-within-feature": { - "id": "path", - "warning": false - } - } -} diff --git a/rust/fixtures/run-configuration/basic.json b/rust/fixtures/run-configuration/basic.json deleted file mode 100644 index aae03705..00000000 --- a/rust/fixtures/run-configuration/basic.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "version": 1, - "generated": { - "version": 1, - "configurations": [ - { - "id": "spring:com.example.DemoApplication", - "name": "DemoApplication", - "type": "spring-boot.maven", - "module": ".", - "mainClass": "com.example.DemoApplication", - "toolchains": { "java": "project-jdk", "maven": "project-maven" }, - "workingDirectory": ".", - "jvmArguments": [], - "programArguments": [], - "mavenProfiles": [] - } - ] - }, - "team": { "version": 1, "configurations": [] }, - "local": { "version": 1, "configurations": [] }, - "expected": { "selectedId": "spring:com.example.DemoApplication", "launchToolchain": "project-maven" } -} diff --git a/rust/fixtures/run-configuration/build-provider.json b/rust/fixtures/run-configuration/build-provider.json index bf80250f..40ab29cd 100644 --- a/rust/fixtures/run-configuration/build-provider.json +++ b/rust/fixtures/run-configuration/build-provider.json @@ -16,8 +16,8 @@ "unrelated": { "version": 2, "toolchain": { - "java": { - "homePath": "/fixture/jdk" + "owner-field": { + "homePath": "/fixture/runtime" } }, "configurations": [] diff --git a/rust/fixtures/run-configuration/configuration-toolchain-selection.json b/rust/fixtures/run-configuration/configuration-toolchain-selection.json deleted file mode 100644 index 7e6217c2..00000000 --- a/rust/fixtures/run-configuration/configuration-toolchain-selection.json +++ /dev/null @@ -1,446 +0,0 @@ -{ - "description": "Canonical reactor paths plus explicit Run overrides, without applying project defaults as configuration overrides or writing source documents.", - "cases": [ - { - "name": "explicit configuration paths override canonical project defaults", - "request": { - "path": ".", - "configurationId": "maven-run", - "localDocument": { - "version": 2, - "toolchain": { - "java": { - "homePath": "C:/Global JDK" - }, - "maven": { - "executablePath": "C:/Global Maven/bin/mvn.cmd", - "javaHomePath": "C:/Global Maven JDK" - }, - "pathOverrides": { - "reactor": { - "javaHomePath": "C:/Reactor JDK", - "mavenExecutablePath": "C:/Reactor Maven/bin/mvn.cmd", - "mavenJavaHomePath": "C:/Reactor Maven JDK" - } - } - }, - "configurations": [ - { - "id": "maven-run", - "extensions": { - "java": { - "homePath": " C:/Run JDK ", - "mavenExecutablePath": "C:/Run Maven/bin/mvn.cmd", - "mavenJavaHomePath": "C:/Run Maven JDK" - } - } - } - ] - } - }, - "generatedDocument": { - "version": 2, - "configurations": [ - { - "id": "maven-run", - "name": "Module", - "provider": "maven.module", - "execution": "task", - "cwd": ".", - "toolchains": { - "java": "project-jdk", - "maven": "project-maven" - }, - "extensions": { - "maven": { - "module": "." - } - } - } - ] - } - }, - { - "name": "empty configuration values inherit the canonical reactor instead of global defaults", - "request": { - "path": "reactor", - "configurationId": "maven-run", - "localDocument": { - "version": 2, - "toolchain": { - "java": { - "homePath": "C:/Global JDK" - }, - "maven": { - "executablePath": "C:/Global Maven/bin/mvn.cmd", - "javaHomePath": "C:/Global Maven JDK" - }, - "pathOverrides": { - "reactor": { - "javaHomePath": "C:/Reactor JDK", - "mavenExecutablePath": "C:/Reactor Maven/bin/mvn.cmd", - "mavenJavaHomePath": "C:/Reactor Maven JDK" - } - } - }, - "configurations": [ - { - "id": "maven-run", - "extensions": { - "java": { - "homePath": "", - "mavenExecutablePath": "", - "mavenJavaHomePath": "" - } - } - } - ] - } - }, - "generatedDocument": { - "version": 2, - "configurations": [ - { - "id": "maven-run", - "name": "Module", - "provider": "maven.module", - "execution": "task", - "cwd": "reactor", - "toolchains": { - "java": "project-jdk", - "maven": "project-maven" - }, - "extensions": { - "maven": { - "module": "." - } - } - } - ] - } - }, - { - "name": "a configuration cannot select another provider root", - "request": { - "path": ".", - "configurationId": "maven-run", - "localDocument": { - "version": 2, - "toolchain": { - "java": { - "homePath": "C:/Global JDK" - }, - "maven": { - "executablePath": "C:/Global Maven/bin/mvn.cmd", - "javaHomePath": "C:/Global Maven JDK" - }, - "pathOverrides": { - "reactor": { - "javaHomePath": "C:/Reactor JDK", - "mavenExecutablePath": "C:/Reactor Maven/bin/mvn.cmd", - "mavenJavaHomePath": "C:/Reactor Maven JDK" - } - } - }, - "configurations": [ - { - "id": "maven-run", - "extensions": { - "java": {} - } - } - ] - } - }, - "generatedDocument": { - "version": 2, - "configurations": [ - { - "id": "maven-run", - "name": "Module", - "provider": "maven.module", - "execution": "task", - "cwd": "other", - "toolchains": { - "java": "project-jdk", - "maven": "project-maven" - }, - "extensions": { - "maven": { - "module": "." - } - } - } - ] - } - }, - { - "name": "an unknown configuration cannot fall back to project paths", - "request": { - "path": ".", - "configurationId": "missing-run", - "localDocument": { - "version": 2, - "toolchain": { - "java": { - "homePath": "C:/Global JDK" - }, - "maven": { - "executablePath": "C:/Global Maven/bin/mvn.cmd", - "javaHomePath": "C:/Global Maven JDK" - }, - "pathOverrides": { - "reactor": { - "javaHomePath": "C:/Reactor JDK", - "mavenExecutablePath": "C:/Reactor Maven/bin/mvn.cmd", - "mavenJavaHomePath": "C:/Reactor Maven JDK" - } - } - }, - "configurations": [ - { - "id": "maven-run", - "extensions": { - "java": {} - } - } - ] - } - }, - "generatedDocument": { - "version": 2, - "configurations": [ - { - "id": "maven-run", - "name": "Module", - "provider": "maven.module", - "execution": "task", - "cwd": ".", - "toolchains": { - "java": "project-jdk", - "maven": "project-maven" - }, - "extensions": { - "maven": { - "module": "." - } - } - } - ] - } - }, - { - "name": "malformed explicit paths cannot fall back to a valid default", - "request": { - "path": ".", - "configurationId": "maven-run", - "localDocument": { - "version": 2, - "toolchain": { - "java": { - "homePath": "C:/Global JDK" - }, - "maven": { - "executablePath": "C:/Global Maven/bin/mvn.cmd", - "javaHomePath": "C:/Global Maven JDK" - }, - "pathOverrides": { - "reactor": { - "javaHomePath": "C:/Reactor JDK", - "mavenExecutablePath": "C:/Reactor Maven/bin/mvn.cmd", - "mavenJavaHomePath": "C:/Reactor Maven JDK" - } - } - }, - "configurations": [ - { - "id": "maven-run", - "extensions": { - "java": { - "mavenExecutablePath": false - } - } - } - ] - } - }, - "generatedDocument": { - "version": 2, - "configurations": [ - { - "id": "maven-run", - "name": "Module", - "provider": "maven.module", - "execution": "task", - "cwd": ".", - "toolchains": { - "java": "project-jdk", - "maven": "project-maven" - }, - "extensions": { - "maven": { - "module": "." - } - } - } - ] - } - }, - { - "name": "team and local explicit fields reuse the existing Run merge", - "request": { - "path": "reactor", - "configurationId": "maven-run", - "localDocument": { - "version": 2, - "toolchain": { - "java": { - "homePath": "C:/Global JDK" - }, - "maven": { - "executablePath": "C:/Global Maven/bin/mvn.cmd", - "javaHomePath": "C:/Global Maven JDK" - }, - "pathOverrides": { - "reactor": { - "javaHomePath": "C:/Reactor JDK", - "mavenExecutablePath": "C:/Reactor Maven/bin/mvn.cmd", - "mavenJavaHomePath": "C:/Reactor Maven JDK" - } - } - }, - "configurations": [ - { - "id": "maven-run", - "extensions": { - "java": { - "mavenExecutablePath": "", - "mavenJavaHomePath": "C:/Local Maven JDK" - } - } - } - ] - } - }, - "teamDocument": { - "version": 2, - "configurations": [ - { - "id": "maven-run", - "extensions": { - "java": { - "homePath": "C:/Team JDK", - "mavenExecutablePath": "C:/Team Maven/bin/mvn.cmd" - } - } - } - ] - }, - "generatedDocument": { - "version": 2, - "configurations": [ - { - "id": "maven-run", - "name": "Module", - "provider": "maven.module", - "execution": "task", - "cwd": "reactor", - "toolchains": { - "java": "project-jdk", - "maven": "project-maven" - }, - "extensions": { - "maven": { - "module": "." - } - } - } - ] - } - }, - { - "name": "saved selectors inherit canonical paths without a generated layer", - "request": { - "path": "reactor", - "configurationId": "user:reference", - "localDocument": { - "version": 2, - "toolchain": { - "maven": { - "executablePath": "C:/Global Maven/bin/mvn.cmd" - }, - "pathOverrides": { - "reactor": { - "mavenExecutablePath": "C:/Reactor Maven/bin/mvn.cmd" - } - } - }, - "configurations": [ - { - "id": "user:reference", - "name": "Reference", - "provider": "build.provider", - "execution": "task", - "extensions": { - "build": { - "pluginID": "dev.example.build", - "buildSystemID": "example", - "root": "reactor", - "run": "module", - "projectID": "p", - "targetID": "t", - "execution": "task", - "userArguments": [] - } - } - } - ] - } - } - } - ], - "version": 1, - "expected": { - "explicit configuration paths override canonical project defaults": { - "toolchain": { - "javaHomePath": "C:/Run JDK", - "mavenExecutablePath": "C:/Run Maven/bin/mvn.cmd", - "mavenJavaHomePath": "C:/Run Maven JDK", - "runtimeExecutablePaths": {} - } - }, - "empty configuration values inherit the canonical reactor instead of global defaults": { - "toolchain": { - "javaHomePath": "C:/Reactor JDK", - "mavenExecutablePath": "C:/Reactor Maven/bin/mvn.cmd", - "mavenJavaHomePath": "C:/Reactor Maven JDK", - "runtimeExecutablePaths": {} - } - }, - "a configuration cannot select another provider root": { - "errorCode": "invalid_request" - }, - "an unknown configuration cannot fall back to project paths": { - "errorCode": "invalid_request" - }, - "malformed explicit paths cannot fall back to a valid default": { - "errorCode": "parse_failed" - }, - "team and local explicit fields reuse the existing Run merge": { - "toolchain": { - "javaHomePath": "C:/Team JDK", - "mavenExecutablePath": "C:/Reactor Maven/bin/mvn.cmd", - "mavenJavaHomePath": "C:/Local Maven JDK", - "runtimeExecutablePaths": {} - } - }, - "saved selectors inherit canonical paths without a generated layer": { - "toolchain": { - "javaHomePath": "", - "mavenExecutablePath": "C:/Reactor Maven/bin/mvn.cmd", - "mavenJavaHomePath": "", - "runtimeExecutablePaths": {} - } - } - } -} diff --git a/rust/fixtures/run-configuration/editor-save.json b/rust/fixtures/run-configuration/editor-save.json index 9afa8597..591ea348 100644 --- a/rust/fixtures/run-configuration/editor-save.json +++ b/rust/fixtures/run-configuration/editor-save.json @@ -5,24 +5,22 @@ "payload": { "root": "/workspace", "scope": "project", - "configurationId": "spring", + "configurationId": "server", "workingDirectory": "backend", - "jvmArguments": "-Xmx2g", "arguments": "--dev", - "environment": { "APP_ENV": "dev" }, - "mavenProfiles": [], - "javaHomePath": "", - "mavenExecutablePath": "", - "mavenJavaHomePath": "", + "environment": { + "APP_ENV": "dev" + }, "toolchain": { - "javaHomePath": "C:/lithe-fixture/jdk-21", - "mavenExecutablePath": "C:/lithe-fixture/apache-maven", - "mavenJavaHomePath": "C:/lithe-fixture/jdk-17" + "runtimeExecutablePaths": { + "project-runtime": "C:/lithe-fixture/runtime/bin/runtime.exe" + } } } }, "expected": { "localDocument": "string", - "projectDocument": "string" + "projectDocument": "string", + "toolchainDocument": "string" } } diff --git a/rust/fixtures/run-configuration/hybrid-service-web.json b/rust/fixtures/run-configuration/hybrid-service-web.json new file mode 100644 index 00000000..b2591df4 --- /dev/null +++ b/rust/fixtures/run-configuration/hybrid-service-web.json @@ -0,0 +1,58 @@ +{ + "version": 1, + "generated": { + "version": 2, + "configurations": [ + { + "id": "build.service:backend", + "name": "backend", + "provider": "build.service", + "execution": "service", + "cwd": ".", + "toolchains": { + "runtime": "project-runtime", + "build": "project-build" + } + }, + { + "id": "npm.script:web/dev", + "name": "dev", + "provider": "npm.script", + "execution": "service", + "command": "npm", + "args": [ + "run", + "dev" + ], + "cwd": "web", + "toolchains": { + "runtime": "project-node" + } + } + ] + }, + "requirements": { + "version": 1, + "toolchains": { + "project-runtime": { + "type": "runtime", + "minimumVersion": "21" + }, + "project-build": { + "type": "build-tool" + }, + "project-node": { + "type": "node", + "minimumVersion": "22" + } + } + }, + "expected": { + "missingNodeDiagnosticConfigurationIds": [ + "npm.script:web/dev" + ], + "unblockedConfigurationIds": [ + "build.service:backend" + ] + } +} diff --git a/rust/fixtures/run-configuration/hybrid-spring-vue.json b/rust/fixtures/run-configuration/hybrid-spring-vue.json deleted file mode 100644 index 1b341033..00000000 --- a/rust/fixtures/run-configuration/hybrid-spring-vue.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "version": 1, - "generated": { - "version": 2, - "configurations": [ - { - "id": "spring-boot.maven:backend", - "name": "backend", - "provider": "spring-boot.maven", - "execution": "service", - "cwd": ".", - "toolchains": { - "java": "project-jdk", - "maven": "project-maven" - } - }, - { - "id": "npm.script:web/dev", - "name": "dev", - "provider": "npm.script", - "execution": "service", - "command": "npm", - "args": ["run", "dev"], - "cwd": "web", - "toolchains": { - "runtime": "project-node" - } - } - ] - }, - "requirements": { - "version": 1, - "toolchains": { - "project-jdk": { "type": "java", "minimumVersion": "21" }, - "project-maven": { "type": "maven", "java": "project-jdk" }, - "project-node": { "type": "node", "minimumVersion": "22" } - } - }, - "expected": { - "missingNodeDiagnosticConfigurationIds": ["npm.script:web/dev"], - "unblockedConfigurationIds": ["spring-boot.maven:backend"] - } -} diff --git a/rust/fixtures/run-configuration/invalid-version.json b/rust/fixtures/run-configuration/invalid-version.json deleted file mode 100644 index cc7a973f..00000000 --- a/rust/fixtures/run-configuration/invalid-version.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "version": 1, - "generated": { "version": 2, "configurations": [] }, - "team": { "version": 1, "configurations": [] }, - "local": { "version": 1, "configurations": [] }, - "expected": { "errorCode": "not_supported" } -} diff --git a/rust/fixtures/run-configuration/maven-module-ownership.json b/rust/fixtures/run-configuration/maven-module-ownership.json deleted file mode 100644 index 23dad637..00000000 --- a/rust/fixtures/run-configuration/maven-module-ownership.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "version": 1, - "files": { - "services/alpha/pom.xml": "4.0.0examplealpha1", - "services/beta/pom.xml": "4.0.0examplebeta1", - "services/alpha/src/main/java/example/Alpha.java": "package example; public class Alpha { public static void main(String[] args) {} }", - "services/beta/src/main/java/example/Beta.java": "package example; public class Beta { public static void main(String[] args) {} }" - }, - "paths": [ - "services/alpha/src/main/java/example/Alpha.java", - "services/beta/src/main/java/example/Beta.java" - ], - "javaEntrypoints": { - "schemaVersion": 1, - "entries": [ - { - "sourcePath": "services/alpha/src/main/java/example/Alpha.java", - "mainClass": "example.Alpha", - "projectName": "alpha" - }, - { - "sourcePath": "services/beta/src/main/java/example/Beta.java", - "mainClass": "example.Beta", - "projectName": "beta" - } - ], - "diagnostics": [] - }, - "local": { - "version": 2, - "configurations": [ - { - "id": "java-main:example.Alpha", - "cwd": ".", - "extensions": { - "maven": { - "reactorPath": "services/beta" - } - } - } - ] - }, - "expected": { - "configurations": [ - { - "id": "java-main:example.Alpha", - "name": "Alpha", - "provider": "java.main", - "execution": "application", - "cwd": ".", - "toolchains": { - "java": "project-jdk", - "maven": "project-maven" - }, - "extensions": { - "maven": { - "module": ".", - "reactorPath": "services/alpha" - } - } - }, - { - "id": "java-main:example.Beta", - "name": "Beta", - "provider": "java.main", - "execution": "application", - "cwd": "services/beta", - "toolchains": { - "java": "project-jdk", - "maven": "project-maven" - }, - "extensions": { - "maven": { - "module": ".", - "reactorPath": "services/beta" - } - } - }, - { - "id": "current-file", - "name": "Current File", - "provider": "java.current-file", - "execution": "application", - "cwd": ".", - "debug": { - "adapter": "jdwp" - } - } - ] - } -} diff --git a/rust/fixtures/run-configuration/multi-module.json b/rust/fixtures/run-configuration/multi-module.json deleted file mode 100644 index d9cc4bbc..00000000 --- a/rust/fixtures/run-configuration/multi-module.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "version": 1, - "generated": { - "version": 1, - "configurations": [ - { - "id": "module:backend", - "name": "backend", - "type": "maven.module", - "module": "backend", - "toolchains": { "java": "project-jdk", "maven": "project-maven" }, - "workingDirectory": ".", - "jvmArguments": [], - "programArguments": [], - "mavenProfiles": [] - }, - { - "id": "module:worker", - "name": "worker", - "type": "maven.module", - "module": "worker", - "toolchains": { "java": "project-jdk", "maven": "project-maven" }, - "workingDirectory": ".", - "jvmArguments": [], - "programArguments": [], - "mavenProfiles": [] - } - ] - }, - "team": { - "version": 1, - "configurations": [{ "id": "module:worker", "mavenProfiles": ["dev"] }] - }, - "local": { - "version": 1, - "configurations": [{ "id": "module:backend", "jvmArguments": ["-Xmx2g"] }] - }, - "expected": { "configurationCount": 2, "selectedId": "module:backend" } -} diff --git a/rust/fixtures/run-configuration/no-entry.json b/rust/fixtures/run-configuration/no-entry.json deleted file mode 100644 index 636fb87f..00000000 --- a/rust/fixtures/run-configuration/no-entry.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "version": 1, - "generated": { "version": 1, "configurations": [] }, - "team": { "version": 1, "configurations": [] }, - "local": { "version": 1, "configurations": [] }, - "expected": { "configurationCount": 0, "state": "noEntries" } -} diff --git a/rust/fixtures/run-configuration/project-environment.json b/rust/fixtures/run-configuration/project-environment.json deleted file mode 100644 index bf70b300..00000000 --- a/rust/fixtures/run-configuration/project-environment.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "version": 1, - "local": { - "version": 2, - "configurations": [ - { "id": "user:service", "extensions": { "java": { "homePath": "/fixture/service-jdk" } } } - ] - }, - "toolchain": { - "javaHomePath": "/fixture/project-jdk", - "mavenExecutablePath": "/fixture/maven", - "mavenJavaHomePath": "", - "pathOverrides": {} - }, - "expected": { - "toolchain": { - "java": { "homePath": "/fixture/project-jdk" }, - "maven": { "executablePath": "/fixture/maven", "javaHomePath": "" }, - "pathOverrides": {} - } - } -} diff --git a/rust/fixtures/run-configuration/project-toolchain-selection.json b/rust/fixtures/run-configuration/project-toolchain-selection.json deleted file mode 100644 index b3a538c0..00000000 --- a/rust/fixtures/run-configuration/project-toolchain-selection.json +++ /dev/null @@ -1,328 +0,0 @@ -{ - "description": "Read-only canonical Run tool paths for a Provider root; platforms discover executables after shared selection.", - "cases": [ - { - "name": "defaults without generated configurations", - "request": { - "path": ".", - "localDocument": { - "version": 2, - "configurations": [], - "toolchain": { - "java": { - "homePath": " C:/Example JDK " - }, - "maven": { - "executablePath": "C:/Example Maven/bin/mvn.cmd", - "javaHomePath": "C:/Maven JDK" - } - } - } - } - }, - { - "name": "nearest reactor override", - "request": { - "path": "reactor/app", - "localDocument": { - "version": 2, - "configurations": [], - "toolchain": { - "java": { - "homePath": " C:/Example JDK " - }, - "maven": { - "executablePath": "C:/Example Maven/bin/mvn.cmd", - "javaHomePath": "C:/Maven JDK" - }, - "pathOverrides": { - "reactor": { - "mavenExecutablePath": "C:/Reactor Maven/mvn.cmd" - } - } - } - } - } - }, - { - "name": "nearest empty fields inherit project rather than an ancestor", - "request": { - "path": "reactor/app/sub", - "localDocument": { - "version": 2, - "configurations": [], - "toolchain": { - "java": { - "homePath": " C:/Example JDK " - }, - "maven": { - "executablePath": "C:/Example Maven/bin/mvn.cmd", - "javaHomePath": "C:/Maven JDK" - }, - "pathOverrides": { - "reactor": { - "javaHomePath": "C:/Parent JDK" - }, - "reactor/app": { - "mavenExecutablePath": "C:/App Maven/mvn.cmd", - "javaHomePath": "" - } - } - } - } - } - }, - { - "name": "path segments do not match adjacent project names", - "request": { - "path": "reactor/app2", - "localDocument": { - "version": 2, - "configurations": [], - "toolchain": { - "java": { - "homePath": " C:/Example JDK " - }, - "maven": { - "executablePath": "C:/Example Maven/bin/mvn.cmd", - "javaHomePath": "C:/Maven JDK" - }, - "pathOverrides": { - "reactor/app": { - "javaHomePath": "C:/Wrong JDK" - } - } - } - } - } - }, - { - "name": "explicit automatic override inherits project defaults", - "request": { - "path": "reactor", - "localDocument": { - "version": 2, - "configurations": [], - "toolchain": { - "java": { - "homePath": " C:/Example JDK " - }, - "maven": { - "executablePath": "C:/Example Maven/bin/mvn.cmd", - "javaHomePath": "C:/Maven JDK" - }, - "pathOverrides": { - "reactor": {} - } - } - } - } - }, - { - "name": "missing toolchain stays automatic", - "request": { - "path": ".", - "localDocument": { - "version": 2, - "configurations": [] - } - } - }, - { - "name": "escaping owner", - "request": { - "path": "../outside", - "localDocument": { - "version": 2, - "configurations": [], - "toolchain": { - "java": { - "homePath": " C:/Example JDK " - }, - "maven": { - "executablePath": "C:/Example Maven/bin/mvn.cmd", - "javaHomePath": "C:/Maven JDK" - } - } - } - } - }, - { - "name": "noncanonical owner", - "request": { - "path": "reactor\\app", - "localDocument": { - "version": 2, - "configurations": [], - "toolchain": { - "java": { - "homePath": " C:/Example JDK " - }, - "maven": { - "executablePath": "C:/Example Maven/bin/mvn.cmd", - "javaHomePath": "C:/Maven JDK" - } - } - } - } - }, - { - "name": "empty owner", - "request": { - "path": "", - "localDocument": { - "version": 2, - "configurations": [], - "toolchain": { - "java": { - "homePath": " C:/Example JDK " - }, - "maven": { - "executablePath": "C:/Example Maven/bin/mvn.cmd", - "javaHomePath": "C:/Maven JDK" - } - } - } - } - }, - { - "name": "ambiguous owner", - "request": { - "path": "reactor/./app", - "localDocument": { - "version": 2, - "configurations": [], - "toolchain": { - "java": { - "homePath": " C:/Example JDK " - }, - "maven": { - "executablePath": "C:/Example Maven/bin/mvn.cmd", - "javaHomePath": "C:/Maven JDK" - } - } - } - } - }, - { - "name": "unknown document version", - "request": { - "path": ".", - "localDocument": { - "version": 99 - } - } - }, - { - "name": "malformed tool path", - "request": { - "path": ".", - "localDocument": { - "version": 2, - "configurations": [], - "toolchain": { - "java": { - "homePath": 42 - } - } - } - } - }, - { - "name": "malformed override", - "request": { - "path": "app", - "localDocument": { - "version": 2, - "configurations": [], - "toolchain": { - "java": { - "homePath": " C:/Example JDK " - }, - "maven": { - "executablePath": "C:/Example Maven/bin/mvn.cmd", - "javaHomePath": "C:/Maven JDK" - }, - "pathOverrides": { - "app": { - "unknownChoice": "value" - } - } - } - } - } - } - ], - "version": 1, - "expected": { - "defaults without generated configurations": { - "toolchain": { - "javaHomePath": "C:/Example JDK", - "mavenExecutablePath": "C:/Example Maven/bin/mvn.cmd", - "mavenJavaHomePath": "C:/Maven JDK", - "runtimeExecutablePaths": {} - } - }, - "nearest reactor override": { - "toolchain": { - "javaHomePath": "C:/Example JDK", - "mavenExecutablePath": "C:/Reactor Maven/mvn.cmd", - "mavenJavaHomePath": "C:/Maven JDK", - "runtimeExecutablePaths": {} - } - }, - "nearest empty fields inherit project rather than an ancestor": { - "toolchain": { - "javaHomePath": "C:/Example JDK", - "mavenExecutablePath": "C:/App Maven/mvn.cmd", - "mavenJavaHomePath": "C:/Maven JDK", - "runtimeExecutablePaths": {} - } - }, - "path segments do not match adjacent project names": { - "toolchain": { - "javaHomePath": "C:/Example JDK", - "mavenExecutablePath": "C:/Example Maven/bin/mvn.cmd", - "mavenJavaHomePath": "C:/Maven JDK", - "runtimeExecutablePaths": {} - } - }, - "explicit automatic override inherits project defaults": { - "toolchain": { - "javaHomePath": "C:/Example JDK", - "mavenExecutablePath": "C:/Example Maven/bin/mvn.cmd", - "mavenJavaHomePath": "C:/Maven JDK", - "runtimeExecutablePaths": {} - } - }, - "missing toolchain stays automatic": { - "toolchain": { - "javaHomePath": "", - "mavenExecutablePath": "", - "mavenJavaHomePath": "", - "runtimeExecutablePaths": {} - } - }, - "escaping owner": { - "errorCode": "invalid_request" - }, - "noncanonical owner": { - "errorCode": "invalid_request" - }, - "empty owner": { - "errorCode": "invalid_request" - }, - "ambiguous owner": { - "errorCode": "invalid_request" - }, - "unknown document version": { - "errorCode": "not_supported" - }, - "malformed tool path": { - "errorCode": "parse_failed" - }, - "malformed override": { - "errorCode": "invalid_request" - } - } -} diff --git a/rust/fixtures/run-configuration/provider-prepared-java.json b/rust/fixtures/run-configuration/provider-prepared-java.json deleted file mode 100644 index 3a594fc7..00000000 --- a/rust/fixtures/run-configuration/provider-prepared-java.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "javaLaunch": { - "mainClass": "com.example.MainTests", - "classPaths": [ - "/workspace/target/test-classes" - ], - "modulePaths": [], - "buildPrepared": true - }, - "version": 1, - "expected": { - "arguments": [ - "com.example.MainTests" - ], - "hasPreLaunchSteps": false - } -} diff --git a/rust/fixtures/run-configuration/provider-project-ownership.json b/rust/fixtures/run-configuration/provider-project-ownership.json deleted file mode 100644 index 76f47307..00000000 --- a/rust/fixtures/run-configuration/provider-project-ownership.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "version": 1, - "files": { - "alpha/pom.xml": "99", - "alpha/app/pom.xml": "", - "alpha/app/custom-checks/demo/Main.java": "package demo; class Main { public static void main(String[] args) {} }", - "beta/pom.xml": "", - "beta/app/sources/demo/Main.java": "package demo; @org.springframework.boot.autoconfigure.SpringBootApplication class Main { public static void main(String[] args) {} }", - ".java-version": "11", - "alpha/mvnw.cmd": "exit /b 99" - }, - "javaEntrypoints": { - "schemaVersion": 1, - "entries": [ - { - "sourcePath": "alpha/app/custom-checks/demo/Main.java", - "mainClass": "demo.Main", - "projectName": "alpha-app" - }, - { - "sourcePath": "beta/app/sources/demo/Main.java", - "mainClass": "demo.Main", - "projectName": "beta-app" - } - ], - "diagnostics": [] - }, - "projectOwnership": [ - { - "path": "alpha", - "contentRoots": [], - "name": "reactor", - "build": { - "pluginID": "dev.example.engine", - "buildSystemID": "engine", - "root": "alpha", - "projectID": "alpha" - } - }, - { - "path": "alpha/app", - "contentRoots": [ - { - "path": "alpha/app/custom-checks", - "kind": "testSource" - }, - { - "path": "alpha/app/generated-checks", - "kind": "generatedTestSource" - } - ], - "name": "app", - "minimumJavaVersion": "17", - "build": { - "pluginID": "dev.example.engine", - "buildSystemID": "engine", - "root": "alpha", - "projectID": "alpha/app" - } - }, - { - "path": "beta/app", - "contentRoots": [ - { - "path": "beta/app/sources", - "kind": "source" - } - ], - "name": "app", - "minimumJavaVersion": "21", - "build": { - "pluginID": "dev.example.engine", - "buildSystemID": "engine", - "root": "beta", - "projectID": "beta/app" - } - } - ], - "expected": { - "configurations": [ - { - "id": "java-main:demo.Main:alpha/app", - "cwd": "alpha", - "module": "app", - "sourceSet": "test" - }, - { - "id": "java-main:demo.Main:beta/app", - "cwd": "beta", - "module": "app", - "sourceSet": "main" - } - ], - "services": [ - { - "id": "build.provider:41b56f3f0d9046e60aad2d801b216bcc301a0e515dcf749413fbabff07b0ece1", - "cwd": "alpha", - "reference": { - "pluginID": "dev.example.engine", - "buildSystemID": "engine", - "root": "alpha", - "run": "serve", - "projectID": "opaque-project-1", - "targetID": "http/opaque:1", - "execution": "service", - "userArguments": [] - } - }, - { - "id": "build.provider:0d63f34f97ab396280a2b2b3226ad1a539db4d4cc35232c79a6ec4acba568ac2", - "cwd": "beta", - "reference": { - "pluginID": "dev.example.engine", - "buildSystemID": "engine", - "root": "beta", - "run": "serve", - "projectID": "opaque-project-2", - "targetID": "http/opaque:2", - "execution": "service", - "userArguments": [] - } - } - ], - "minimumJavaVersion": "21" - }, - "providerRuns": [ - { - "name": "Application service", - "reference": { - "pluginID": "dev.example.engine", - "buildSystemID": "engine", - "root": "alpha", - "run": "serve", - "projectID": "opaque-project-1", - "targetID": "http/opaque:1", - "execution": "service", - "userArguments": [] - }, - "projectPath": "alpha/app" - }, - { - "name": "Application service", - "reference": { - "pluginID": "dev.example.engine", - "buildSystemID": "engine", - "root": "beta", - "run": "serve", - "projectID": "opaque-project-2", - "targetID": "http/opaque:2", - "execution": "service", - "userArguments": [] - }, - "projectPath": "beta/app" - } - ] -} diff --git a/rust/fixtures/run-configuration/single-module.json b/rust/fixtures/run-configuration/single-module.json deleted file mode 100644 index 94f6fbd1..00000000 --- a/rust/fixtures/run-configuration/single-module.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "version": 1, - "generated": { - "version": 1, - "configurations": [ - { - "id": "spring:com.example.App", - "name": "App", - "type": "spring-boot.maven", - "module": ".", - "mainClass": "com.example.App", - "toolchains": { "java": "project-jdk", "maven": "project-maven" }, - "workingDirectory": ".", - "jvmArguments": [], - "programArguments": [], - "mavenProfiles": [] - } - ] - }, - "team": { "version": 1, "configurations": [] }, - "local": { "version": 1, "configurations": [] }, - "expected": { "configurationCount": 1, "selectedId": "spring:com.example.App" } -} diff --git a/rust/fixtures/run-configuration/subproject-toolchains.json b/rust/fixtures/run-configuration/subproject-toolchains.json deleted file mode 100644 index 03e10773..00000000 --- a/rust/fixtures/run-configuration/subproject-toolchains.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "version": 1, - "toolchain": { - "javaHomePath": "/fixture/project-jdk", - "pathOverrides": { - "services/a": { - "javaHomePath": "/fixture/a-jdk", - "mavenExecutablePath": "/fixture/a maven/bin/mvn", - "mavenJavaHomePath": "" - }, - "services/b": { - "javaHomePath": "", - "mavenExecutablePath": "./mvnw", - "mavenJavaHomePath": "/fixture/b-maven-jdk" - } - } - }, - "invalidRoots": [ - "", - ".", - "..", - "../outside", - "/absolute", - "a/../b", - "a/./b", - "a//b", - "a/", - "C:/absolute", - "a\\b" - ], - "expected": { - "pathOverrides": { - "services/a": { - "javaHomePath": "/fixture/a-jdk", - "mavenExecutablePath": "/fixture/a maven/bin/mvn", - "mavenJavaHomePath": "" - }, - "services/b": { - "javaHomePath": "", - "mavenExecutablePath": "./mvnw", - "mavenJavaHomePath": "/fixture/b-maven-jdk" - } - } - } -} diff --git a/rust/fixtures/run-configuration/toolchain-mismatch.json b/rust/fixtures/run-configuration/toolchain-mismatch.json deleted file mode 100644 index 15a29a3b..00000000 --- a/rust/fixtures/run-configuration/toolchain-mismatch.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "version": 1, - "requirements": { - "version": 1, - "toolchains": { - "project-jdk": { "type": "java", "minimumVersion": "21", "preferredVendor": "temurin" } - } - }, - "candidates": [ - { "id": "project-jdk", "type": "java", "version": "17.0.12", "vendor": "Zulu" } - ], - "expected": { - "diagnosticCodes": ["toolchainVersionMismatch", "toolchainVendorMismatch"] - } -} diff --git a/rust/fixtures/spring/basic.json b/rust/fixtures/spring/basic.json deleted file mode 100644 index 1d5c134c..00000000 --- a/rust/fixtures/spring/basic.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "Spring workspace semantic index", - "request": { - "command": "spring.index", - "payload": { - "root": "/fixture/workspace", - "paths": [ - "src/main/java/example/DemoProperties.java", - "src/main/java/example/GreetingController.java", - "src/main/resources/application-dev.yml" - ], - "metadataRepositories": [ - "/fixture/gradle-repository", - "/fixture/maven-repository" - ], - "refreshDependencyMetadata": true, - "textOverrides": { - "src/main/resources/application-dev.yml": "demo:\n enabled: true\n" - } - } - }, - "expected": { - "property": { - "name": "demo.enabled", - "sourcePath": "src/main/java/example/DemoProperties.java", - "sourceLine": 5 - }, - "value": { - "path": "src/main/resources/application-dev.yml", - "profile": "dev", - "overridesBaseValue": false - }, - "propertyReference": { - "key": "demo.enabled" - }, - "injection": { - "typeName": "GreetingService", - "qualifier": "primaryGreeting" - }, - "endpoint": { - "httpMethods": ["GET"], - "route": "/api/greeting" - } - } -} diff --git a/rust/lithe-core/Cargo.toml b/rust/lithe-core/Cargo.toml index f5ddd096..17aeb1fa 100644 --- a/rust/lithe-core/Cargo.toml +++ b/rust/lithe-core/Cargo.toml @@ -35,8 +35,6 @@ sha2 = "0.10" shell-words = "=1.1.1" toml = { version = "1.1.4", default-features = false, features = ["parse", "serde"] } toml_edit = "0.25" -tree-sitter = "0.25" -tree-sitter-java = "0.23" serde_yaml_ng = "0.10.0" similar = { version = "3.2", default-features = false, features = ["std"] } zip = { version = "2.4", default-features = false, features = ["deflate"] } diff --git a/rust/lithe-core/include/lithe_core.h b/rust/lithe-core/include/lithe_core.h index fbcc8145..ad4c2da1 100644 --- a/rust/lithe-core/include/lithe_core.h +++ b/rust/lithe-core/include/lithe_core.h @@ -16,7 +16,6 @@ char *lithe_core_execute_json(const char *request); /* Immutable source borrowed only until return; response uses lithe_core_free_string. */ char *lithe_core_execute_document_json(const char *request, const uint8_t *bytes, size_t length); char *lithe_core_execute_json_with_events(const char *request, void (*callback)(const char *, void *), void *context); -char *lithe_core_lsp_provider_catalog_json(const char *workspace_root); int32_t lithe_core_cancel(const char *operation_id); void lithe_core_free_string(char *value); void lithe_core_install_log_waker(void (*callback)(void *), void *context); diff --git a/rust/lithe-core/resources/lsp/language-providers.json b/rust/lithe-core/resources/lsp/language-providers.json deleted file mode 100644 index 4275a3d3..00000000 --- a/rust/lithe-core/resources/lsp/language-providers.json +++ /dev/null @@ -1,500 +0,0 @@ -{ - "version": 2, - "providers": [ - { - "id": "java", - "displayName": "Java", - "fileExtensions": ["java"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "java", - "languageServerLaunch": { - "executableNames": ["jdtls"], - "arguments": [] - }, - "languageServerInstallation": { - "homebrewFormula": "jdtls", - "officialDownloadURL": "https://download.eclipse.org/jdtls/milestones/" - } - }, - { - "id": "go", - "displayName": "Go", - "fileExtensions": ["go"], - "capabilities": ["run", "languageServer", "debugAdapter", "formatting", "testing"], - "activationPolicy": "onDemand", - "languageId": "go", - "languageServerLaunch": { - "executableNames": ["gopls"], - "arguments": [] - }, - "languageServerInstallation": { - "homebrewFormula": "gopls", - "officialDownloadURL": "https://go.dev/gopls/" - } - }, - { - "id": "python", - "displayName": "Python", - "fileExtensions": ["py", "pyw"], - "capabilities": ["run", "languageServer", "formatting", "testing"], - "activationPolicy": "onDemand", - "languageId": "python", - "languageServerLaunch": { - "executableNames": ["basedpyright-langserver", "pyright-langserver"], - "arguments": ["--stdio"] - }, - "languageServerInstallation": { - "homebrewFormula": "pyright", - "officialDownloadURL": "https://github.com/microsoft/pyright" - } - }, - { - "id": "node", - "displayName": "Node.js", - "fileExtensions": ["js", "jsx", "ts", "tsx", "mjs", "cjs"], - "capabilities": ["run", "languageServer", "debugAdapter", "formatting", "testing"], - "activationPolicy": "onDemand", - "languageId": "javascript", - "languageServerLaunch": { - "executableNames": ["typescript-language-server"], - "arguments": ["--stdio"] - }, - "languageServerInstallation": { - "homebrewFormula": "typescript-language-server", - "officialDownloadURL": "https://github.com/typescript-language-server/typescript-language-server" - }, - "languageIdsByExtension": { - "jsx": "javascriptreact", - "ts": "typescript", - "tsx": "typescriptreact" - } - }, - { - "id": "rust", - "displayName": "Rust", - "fileExtensions": ["rs"], - "capabilities": ["run", "languageServer", "formatting", "testing"], - "activationPolicy": "onDemand", - "languageId": "rust", - "languageServerLaunch": { - "executableNames": ["rust-analyzer"], - "arguments": [], - "validationArguments": ["--version"] - }, - "languageServerInstallation": { - "homebrewFormula": "rust-analyzer", - "officialDownloadURL": "https://rust-analyzer.github.io/manual.html#installation" - } - }, - { - "id": "clangd", - "displayName": "C/C++/Objective-C", - "fileExtensions": ["c", "h", "hh", "hpp", "hxx", "cpp", "cc", "cxx", "m", "mm"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "cpp", - "languageServerLaunch": { - "executableNames": ["clangd"], - "arguments": [] - }, - "languageServerInstallation": { - "homebrewFormula": "llvm", - "officialDownloadURL": "https://clangd.llvm.org/installation" - }, - "languageIdsByExtension": { - "c": "c", - "h": "c", - "m": "objective-c", - "mm": "objective-cpp" - } - }, - { - "id": "csharp", - "displayName": "C#", - "fileExtensions": ["cs", "csx"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "csharp" - }, - { - "id": "fsharp", - "displayName": "F#", - "fileExtensions": ["fs", "fsi", "fsx"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "fsharp" - }, - { - "id": "swift", - "displayName": "Swift", - "fileExtensions": ["swift"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "swift", - "languageServerLaunch": { - "executableNames": ["sourcekit-lsp"], - "arguments": [] - }, - "languageServerInstallation": { - "officialDownloadURL": "https://github.com/swiftlang/sourcekit-lsp" - } - }, - { - "id": "kotlin", - "displayName": "Kotlin", - "fileExtensions": ["kt", "kts"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "kotlin", - "languageServerLaunch": { - "executableNames": ["kotlin-language-server"], - "arguments": [] - }, - "languageServerInstallation": { - "officialDownloadURL": "https://github.com/Kotlin/kotlin-lsp" - } - }, - { - "id": "scala", - "displayName": "Scala", - "fileExtensions": ["scala", "sc"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "scala", - "languageServerLaunch": { - "executableNames": ["metals"], - "arguments": [] - }, - "languageServerInstallation": { - "homebrewFormula": "metals", - "officialDownloadURL": "https://scalameta.org/metals/docs/editors/overview.html" - } - }, - { - "id": "groovy", - "displayName": "Groovy", - "fileExtensions": ["groovy"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "groovy" - }, - { - "id": "ruby", - "displayName": "Ruby", - "fileExtensions": ["rb", "rake", "gemspec"], - "fileNames": ["Rakefile", "Gemfile"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "ruby", - "languageServerLaunch": { - "executableNames": ["ruby-lsp"], - "arguments": [] - }, - "languageServerInstallation": { - "officialDownloadURL": "https://shopify.github.io/ruby-lsp/" - } - }, - { - "id": "php", - "displayName": "PHP", - "fileExtensions": ["php", "phtml"], - "capabilities": ["run", "languageServer", "formatting", "testing"], - "activationPolicy": "onDemand", - "languageId": "php", - "languageServerLaunch": { - "executableNames": ["intelephense"], - "arguments": ["--stdio"] - }, - "languageServerInstallation": { - "officialDownloadURL": "https://intelephense.com/" - } - }, - { - "id": "dart", - "displayName": "Dart", - "fileExtensions": ["dart"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "dart" - }, - { - "id": "lua", - "displayName": "Lua", - "fileExtensions": ["lua"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "lua" - }, - { - "id": "shell", - "displayName": "Shell", - "fileExtensions": ["sh", "bash", "zsh", "fish", "ksh"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "shellscript", - "languageServerLaunch": { - "executableNames": ["bash-language-server"], - "arguments": ["start"] - }, - "languageServerInstallation": { - "officialDownloadURL": "https://github.com/bash-lsp/bash-language-server" - } - }, - { - "id": "powershell", - "displayName": "PowerShell", - "fileExtensions": ["ps1", "psm1", "psd1"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "powershell" - }, - { - "id": "html", - "displayName": "HTML", - "fileExtensions": ["html", "htm", "xhtml"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "html" - }, - { - "id": "css", - "displayName": "CSS", - "fileExtensions": ["css", "scss", "sass", "less"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "css", - "languageIdsByExtension": { - "less": "less", - "sass": "sass", - "scss": "scss" - } - }, - { - "id": "vue", - "displayName": "Vue", - "fileExtensions": ["vue"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "vue" - }, - { - "id": "svelte", - "displayName": "Svelte", - "fileExtensions": ["svelte"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "svelte" - }, - { - "id": "astro", - "displayName": "Astro", - "fileExtensions": ["astro"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "astro" - }, - { - "id": "json", - "displayName": "JSON", - "fileExtensions": ["json", "jsonc", "json5"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "json", - "languageIdsByExtension": { - "jsonc": "jsonc" - } - }, - { - "id": "yaml", - "displayName": "YAML", - "fileExtensions": ["yml", "yaml"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "yaml", - "languageServerLaunch": { - "executableNames": ["yaml-language-server"], - "arguments": ["--stdio"] - }, - "languageServerInstallation": { - "homebrewFormula": "yaml-language-server", - "officialDownloadURL": "https://github.com/redhat-developer/yaml-language-server" - } - }, - { - "id": "xml", - "displayName": "XML", - "fileExtensions": ["xml", "xsd", "wsdl", "pom"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "xml" - }, - { - "id": "markdown", - "displayName": "Markdown", - "fileExtensions": ["md", "markdown", "mdx"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "markdown" - }, - { - "id": "sql", - "displayName": "SQL", - "fileExtensions": ["sql"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "sql" - }, - { - "id": "terraform", - "displayName": "Terraform", - "fileExtensions": ["tf", "tfvars"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "terraform" - }, - { - "id": "dockerfile", - "displayName": "Dockerfile", - "fileExtensions": ["dockerfile"], - "fileNames": ["Dockerfile"], - "fileNamePrefixes": ["Dockerfile."], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "dockerfile", - "languageIdsByFileName": { - "Dockerfile": "dockerfile" - } - }, - { - "id": "cmake", - "displayName": "CMake", - "fileExtensions": ["cmake"], - "fileNames": ["CMakeLists.txt"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "cmake" - }, - { - "id": "make", - "displayName": "Make", - "fileExtensions": ["mk"], - "fileNames": ["Makefile", "GNUmakefile"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "makefile" - }, - { - "id": "toml", - "displayName": "TOML", - "fileExtensions": ["toml"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "toml" - }, - { - "id": "graphql", - "displayName": "GraphQL", - "fileExtensions": ["graphql", "gql"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "graphql" - }, - { - "id": "protobuf", - "displayName": "Protocol Buffers", - "fileExtensions": ["proto"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "proto" - }, - { - "id": "prisma", - "displayName": "Prisma", - "fileExtensions": ["prisma"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "prisma" - }, - { - "id": "elixir", - "displayName": "Elixir", - "fileExtensions": ["ex", "exs"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "elixir" - }, - { - "id": "erlang", - "displayName": "Erlang", - "fileExtensions": ["erl", "hrl"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "erlang" - }, - { - "id": "haskell", - "displayName": "Haskell", - "fileExtensions": ["hs", "lhs"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "haskell" - }, - { - "id": "ocaml", - "displayName": "OCaml", - "fileExtensions": ["ml", "mli"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "ocaml" - }, - { - "id": "clojure", - "displayName": "Clojure", - "fileExtensions": ["clj", "cljs", "cljc", "edn"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "clojure" - }, - { - "id": "julia", - "displayName": "Julia", - "fileExtensions": ["jl"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "julia" - }, - { - "id": "r", - "displayName": "R", - "fileExtensions": ["r"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "r" - }, - { - "id": "perl", - "displayName": "Perl", - "fileExtensions": ["pl", "pm", "t"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "perl" - }, - { - "id": "zig", - "displayName": "Zig", - "fileExtensions": ["zig"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "zig" - }, - { - "id": "solidity", - "displayName": "Solidity", - "fileExtensions": ["sol"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "solidity" - } - ] -} diff --git a/rust/lithe-core/src/build/run_start.rs b/rust/lithe-core/src/build/run_start.rs index f8861c3d..f6cca314 100644 --- a/rust/lithe-core/src/build/run_start.rs +++ b/rust/lithe-core/src/build/run_start.rs @@ -57,11 +57,16 @@ struct StartParams { rerun: Option, } +/// Exactly one of `contents` and `base64` carries the file. Text is written as UTF-8; +/// bytes are written as given, for a format whose encoding the plugin must choose itself. #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] struct TemporaryFile { name: String, - contents: String, + #[serde(default)] + contents: Option, + #[serde(default)] + base64: Option, } #[derive(Debug, Deserialize)] @@ -158,7 +163,10 @@ fn decode_start(payload: &DecodeStartPayload, required_tools: &[String]) -> Vali "environment": environment, "temporaryFiles": files .iter() - .map(|file| json!({"path": file.path, "contents": file.contents})) + .map(|file| match &file.contents { + PlannedContents::Text(text) => json!({"path": file.path, "contents": text}), + PlannedContents::Bytes(base64) => json!({"path": file.path, "base64": base64}), + }) .collect::>(), }); if let Some(debug) = ¶ms.debug { @@ -226,7 +234,13 @@ fn executable(process: &Map, required_tools: &[String]) -> Valida struct PlannedFile { name: String, path: String, - contents: String, + contents: PlannedContents, +} + +/// What the host writes: UTF-8 text, or bytes as validated standard base64. +enum PlannedContents { + Text(String), + Bytes(String), } fn temporary_files( @@ -265,21 +279,50 @@ fn temporary_files( "A temporary file name is unique, 1 to 64 letters, digits, '.', '_' or '-', and starts with a letter or digit.", )); } - if file.contents.len() > MAX_TEMPORARY_FILE_BYTES || file.contents.contains('\0') { - return Err(invalid( - &format!("temporaryFiles[{index}].contents"), - "A temporary file holds at most 4 MiB of text.", - )); - } + let contents = temporary_contents(file, index)?; planned.push(PlannedFile { name: file.name.clone(), path: format!("{directory}{}{}", std::path::MAIN_SEPARATOR, file.name), - contents: file.contents.clone(), + contents, }); } Ok(planned) } +/// Text without NUL or standard-base64 bytes, each at most 4 MiB once written. +fn temporary_contents(file: &TemporaryFile, index: usize) -> Validation { + use base64::Engine as _; + match (&file.contents, &file.base64) { + (Some(text), None) => { + if text.len() > MAX_TEMPORARY_FILE_BYTES || text.contains('\0') { + return Err(invalid( + &format!("temporaryFiles[{index}].contents"), + "A temporary file holds at most 4 MiB of text.", + )); + } + Ok(PlannedContents::Text(text.clone())) + } + (None, Some(encoded)) => { + // Bound the encoded form first so an oversized string is never decoded. + let fits = encoded.len() <= MAX_TEMPORARY_FILE_BYTES.div_ceil(3) * 4 + && base64::engine::general_purpose::STANDARD + .decode(encoded) + .is_ok_and(|bytes| bytes.len() <= MAX_TEMPORARY_FILE_BYTES); + if !fits { + return Err(invalid( + &format!("temporaryFiles[{index}].base64"), + "A temporary file holds at most 4 MiB of bytes in standard base64.", + )); + } + Ok(PlannedContents::Bytes(encoded.clone())) + } + _ => Err(invalid( + &format!("temporaryFiles[{index}]"), + "A temporary file has either text contents or base64 bytes.", + )), + } +} + fn valid_temporary_name(name: &str) -> bool { name.len() <= MAX_TEMPORARY_NAME_BYTES && name diff --git a/rust/lithe-core/src/debug/engine.rs b/rust/lithe-core/src/debug/engine.rs index 36561003..2775393e 100644 --- a/rust/lithe-core/src/debug/engine.rs +++ b/rust/lithe-core/src/debug/engine.rs @@ -1013,7 +1013,7 @@ pub(crate) fn disconnect(request: DisconnectRequest) -> Result, } diff --git a/rust/lithe-core/src/execution/launch_plan.rs b/rust/lithe-core/src/execution/launch_plan.rs deleted file mode 100644 index 00abd4a7..00000000 --- a/rust/lithe-core/src/execution/launch_plan.rs +++ /dev/null @@ -1,451 +0,0 @@ -//! Plans Java launches whose command line exceeds the operating system limit. -//! -//! Oversized path-list options (`-cp`, `-classpath`, `--class-path`, `-p`, -//! `--module-path`) move into a JDK argument file. The JVM options before the -//! application target and every program argument stay on the command line. - -use crate::protocol::{CoreError, ErrorCode}; -use serde::{Deserialize, Serialize}; - -/// Windows refuses a CreateProcess command line of 32,767 UTF-16 code units. -/// The 2048-unit headroom covers the terminating null and host quoting. -const DEFAULT_COMMAND_LINE_LIMIT: usize = 32767 - 2048; - -/// The JDK reads `@argfile` references from JDK 9 onward. -const ARGUMENT_FILE_MIN_FEATURE_VERSION: u32 = 9; - -/// Options whose value is a path list; they carry nearly all of an oversized command line. -const PATH_OPTIONS: [&str; 5] = ["-cp", "-classpath", "--class-path", "-p", "--module-path"]; - -/// Launcher options whose following argument is a value, not the application target. -const VALUE_OPTIONS: [&str; 7] = [ - "--add-modules", - "--add-exports", - "--add-opens", - "--add-reads", - "--patch-module", - "--limit-modules", - "--upgrade-module-path", -]; - -/// A Java launch request. The executable is the launcher path; the JDK feature version -/// is either explicit or derived from host-read `release` contents, nearest first. -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LaunchCommandRequest { - /// Launcher path as the host will spawn it. - pub executable: String, - /// Complete argument list, excluding the executable. - #[serde(default)] - pub arguments: Vec, - /// Known JDK feature version. Takes precedence over `java_release_contents`. - pub java_feature_version: Option, - /// Host-read `release` file contents, ordered nearest first. Core reads the first - /// entry that contains `JAVA_VERSION`; an unrecognized value is unknown. - pub java_release_contents: Option>, - /// Command-line byte budget. Defaults to the Windows-safe budget. - pub command_line_limit: Option, -} - -/// The launch the host spawns. -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct LaunchCommandPlan { - /// Arguments to spawn. When `argument_file` is set, the element at - /// `argument_file.argument_index` is an empty placeholder that the host replaces - /// with `@`. - pub arguments: Vec, - /// Argument-file text to write, or `None` when the launch is unchanged. - pub argument_file: Option, -} - -/// Argument-file text and the index of the placeholder that references it. -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct LaunchArgumentFile { - /// Unicode text in the JDK argument-file quoting format. - pub text: String, - /// Index into `LaunchCommandPlan::arguments` of the placeholder. - pub argument_index: usize, -} - -/// Plans a launch. Only a Java launcher of feature version 9 or later whose command line -/// exceeds the limit is changed; every other launch is returned unchanged. -pub fn plan_launch_command(request: &LaunchCommandRequest) -> Result { - if request.executable.is_empty() { - return Err(CoreError::new( - ErrorCode::InvalidRequest, - "Launch command has no executable.", - )); - } - let unchanged = || LaunchCommandPlan { - arguments: request.arguments.clone(), - argument_file: None, - }; - let limit = request - .command_line_limit - .unwrap_or(DEFAULT_COMMAND_LINE_LIMIT); - if !is_java_launcher(&request.executable) - || command_line_size(&request.executable, &request.arguments) <= limit - { - return Ok(unchanged()); - } - let version = request.java_feature_version.or_else(|| { - request - .java_release_contents - .as_deref() - .and_then(feature_version_from_nearest) - }); - match version { - Some(version) if version >= ARGUMENT_FILE_MIN_FEATURE_VERSION => {} - _ => return Ok(unchanged()), - } - - let mut arguments = Vec::with_capacity(request.arguments.len()); - let mut body = String::new(); - let mut placeholder = None; - let mut index = 0; - while index < request.arguments.len() { - let argument = request.arguments[index].as_str(); - // The application target (main class, -jar, -m/--module) ends the JVM options; - // it and everything after it are program arguments and stay verbatim. - if is_application_target(argument) || !argument.starts_with('-') { - arguments.extend(request.arguments[index..].iter().cloned()); - break; - } - if PATH_OPTIONS.contains(&argument) && index + 1 < request.arguments.len() { - placeholder.get_or_insert_with(|| { - arguments.push(String::new()); - arguments.len() - 1 - }); - push_argument_file_pair(&mut body, argument, &request.arguments[index + 1]); - index += 2; - continue; - } - // The --class-path=value spelling is one argument and moves the same way. - if let Some((name, value)) = argument.split_once('=') { - if PATH_OPTIONS.contains(&name) { - placeholder.get_or_insert_with(|| { - arguments.push(String::new()); - arguments.len() - 1 - }); - push_argument_file_pair(&mut body, name, value); - index += 1; - continue; - } - } - if VALUE_OPTIONS.contains(&argument) && index + 1 < request.arguments.len() { - arguments.push(argument.to_owned()); - arguments.push(request.arguments[index + 1].clone()); - index += 2; - continue; - } - arguments.push(argument.to_owned()); - index += 1; - } - - match placeholder { - Some(argument_index) => Ok(LaunchCommandPlan { - arguments, - argument_file: Some(LaunchArgumentFile { - text: body, - argument_index, - }), - }), - None => Ok(unchanged()), - } -} - -/// Reads the JDK feature version from the text of one `release` file. Legacy `1.x` -/// versions report the second number, so `1.8.0_392` is 8 and `17.0.2` is 17. -pub fn java_feature_version_from_release(contents: &str) -> Option { - parse_feature_version(release_java_version(contents)?) -} - -/// Returns the raw value of the first `JAVA_VERSION=` line, without surrounding spaces. -fn release_java_version(contents: &str) -> Option<&str> { - contents - .lines() - .find_map(|line| line.trim().strip_prefix("JAVA_VERSION=")) -} - -/// Uses the nearest release file that names a `JAVA_VERSION`. A farther file never -/// answers for an unrecognized nearer one. -fn feature_version_from_nearest(contents: &[String]) -> Option { - let nearest = contents - .iter() - .find(|text| release_java_version(text).is_some())?; - java_feature_version_from_release(nearest) -} - -fn parse_feature_version(value: &str) -> Option { - let parts: Vec<&str> = value - .trim_matches('"') - .split(|c: char| matches!(c, '.' | '_' | '-' | '+')) - .filter(|part| !part.is_empty()) - .collect(); - let first: u32 = parts.first()?.parse().ok()?; - if first == 1 && parts.len() > 1 { - parts[1].parse().ok() - } else { - Some(first) - } -} - -fn is_application_target(argument: &str) -> bool { - matches!(argument, "-jar" | "-m" | "--module") || argument.starts_with("--module=") -} - -/// Accepts the launcher names of every platform, with or without the `.exe` suffix. -fn is_java_launcher(executable: &str) -> bool { - let name = executable - .rsplit(|c: char| c == '/' || c == '\\') - .next() - .unwrap_or(executable) - .to_ascii_lowercase(); - matches!(name.as_str(), "java" | "java.exe" | "javaw" | "javaw.exe") -} - -/// Estimates the command line: each argument carries a quote and a separator. -fn command_line_size(executable: &str, arguments: &[String]) -> usize { - arguments - .iter() - .fold(executable.len() + 3, |size, argument| { - size + argument.len() + 3 - }) -} - -fn push_argument_file_pair(body: &mut String, option: &str, value: &str) { - push_argument_file_token(body, option); - push_argument_file_token(body, value); -} - -/// Quotes one token the way the JDK argument-file reader expects: double quotes, with -/// backslashes and double quotes escaped. -fn push_argument_file_token(body: &mut String, token: &str) { - body.push('"'); - for character in token.chars() { - if character == '\\' || character == '"' { - body.push('\\'); - } - body.push(character); - } - body.push_str("\"\n"); -} - -#[cfg(test)] -mod tests { - use super::*; - - fn request(executable: &str, arguments: &[&str]) -> LaunchCommandRequest { - LaunchCommandRequest { - executable: executable.to_owned(), - arguments: arguments.iter().map(|value| (*value).to_owned()).collect(), - java_feature_version: Some(17), - java_release_contents: None, - command_line_limit: Some(40), - } - } - - fn unchanged(request: &LaunchCommandRequest) -> LaunchCommandPlan { - LaunchCommandPlan { - arguments: request.arguments.clone(), - argument_file: None, - } - } - - #[test] - fn launch_plan_keeps_a_command_line_within_the_limit() { - let mut request = request("/jdk/bin/java", &["-cp", "a.jar", "Main"]); - request.command_line_limit = None; - assert_eq!(plan_launch_command(&request).unwrap(), unchanged(&request)); - } - - #[test] - fn launch_plan_moves_an_oversized_classpath_into_an_argument_file() { - let request = request( - "/jdk/bin/java", - &[ - "-Xmx1g", - "-cp", - "lib/one.jar:lib/two.jar", - "com.example.Main", - "--flag", - ], - ); - let plan = plan_launch_command(&request).unwrap(); - assert_eq!(plan.arguments, ["-Xmx1g", "", "com.example.Main", "--flag"]); - assert_eq!( - plan.argument_file, - Some(LaunchArgumentFile { - text: "\"-cp\"\n\"lib/one.jar:lib/two.jar\"\n".to_owned(), - argument_index: 1, - }) - ); - } - - #[test] - fn launch_plan_escapes_spaces_quotes_and_backslashes_in_values() { - let request = request( - "C:\\jdk\\bin\\java.exe", - &["-classpath", "C:\\my libs\\\"x\".jar", "Main"], - ); - let plan = plan_launch_command(&request).unwrap(); - let file = plan.argument_file.unwrap(); - assert_eq!( - file.text, - "\"-classpath\"\n\"C:\\\\my libs\\\\\\\"x\\\".jar\"\n" - ); - } - - #[test] - fn launch_plan_moves_module_path_in_both_spellings() { - let request = request( - "/jdk/bin/java", - &["-p", "mods/a", "--module-path=mods/b", "com.example.Main"], - ); - let plan = plan_launch_command(&request).unwrap(); - assert_eq!(plan.arguments, ["", "com.example.Main"]); - assert_eq!( - plan.argument_file.unwrap().text, - "\"-p\"\n\"mods/a\"\n\"--module-path\"\n\"mods/b\"\n" - ); - } - - #[test] - fn launch_plan_leaves_java_older_than_nine_unchanged() { - let mut request = request("/jdk/bin/java", &["-cp", "a.jar", "Main"]); - request.java_feature_version = Some(8); - assert_eq!(plan_launch_command(&request).unwrap(), unchanged(&request)); - } - - #[test] - fn launch_plan_leaves_an_unknown_jdk_version_unchanged() { - let mut request = request("/jdk/bin/java", &["-cp", "a.jar", "Main"]); - request.java_feature_version = None; - request.java_release_contents = None; - assert_eq!(plan_launch_command(&request).unwrap(), unchanged(&request)); - } - - #[test] - fn launch_plan_leaves_non_java_launches_unchanged() { - let request = request("/usr/bin/node", &["-cp", "a.jar", "Main"]); - assert_eq!(plan_launch_command(&request).unwrap(), unchanged(&request)); - } - - #[test] - fn launch_plan_preserves_program_arguments_after_the_main_class() { - let request = request( - "/jdk/bin/java", - &["-cp", "a.jar", "com.example.Main", "-cp", "user-value"], - ); - let plan = plan_launch_command(&request).unwrap(); - assert_eq!( - plan.arguments, - ["", "com.example.Main", "-cp", "user-value"] - ); - assert_eq!(plan.argument_file.unwrap().text, "\"-cp\"\n\"a.jar\"\n"); - } - - #[test] - fn launch_plan_stops_at_jar_and_module_targets() { - let jar = request("/jdk/bin/java", &["-jar", "app.jar", "-cp", "x"]); - assert_eq!(plan_launch_command(&jar).unwrap(), unchanged(&jar)); - - let module = request( - "/jdk/bin/java", - &["--module", "com.example/com.example.Main", "-p", "mods"], - ); - assert_eq!(plan_launch_command(&module).unwrap(), unchanged(&module)); - } - - #[test] - fn launch_plan_keeps_value_options_intact() { - let request = request( - "/jdk/bin/java", - &["--add-modules", "-cp", "-cp", "a.jar", "Main"], - ); - let plan = plan_launch_command(&request).unwrap(); - assert_eq!(plan.arguments, ["--add-modules", "-cp", "", "Main"]); - } - - #[test] - fn launch_plan_reads_the_release_version_forms() { - assert_eq!( - java_feature_version_from_release("JAVA_VERSION=\"1.8.0_392\"\n"), - Some(8) - ); - assert_eq!( - java_feature_version_from_release("IMPLEMENTOR=\"x\"\nJAVA_VERSION=\"17.0.2\"\n"), - Some(17) - ); - assert_eq!( - java_feature_version_from_release("JAVA_VERSION=\"21\""), - Some(21) - ); - assert_eq!(java_feature_version_from_release("IMPLEMENTOR=\"x\""), None); - assert_eq!( - java_feature_version_from_release("JAVA_VERSION=\"weird\""), - None - ); - } - - #[test] - fn launch_plan_takes_the_nearest_release_and_does_not_fall_back() { - let mut request = request("/jdk/bin/java", &["-cp", "a.jar", "Main"]); - request.java_feature_version = None; - request.java_release_contents = Some(vec![ - "JAVA_VERSION=\"weird\"".to_owned(), - "JAVA_VERSION=\"17\"".to_owned(), - ]); - assert_eq!(plan_launch_command(&request).unwrap(), unchanged(&request)); - - request.command_line_limit = Some(10); - request.java_release_contents = Some(vec![ - "IMPLEMENTOR=\"x\"".to_owned(), - "JAVA_VERSION=\"21\"".to_owned(), - ]); - assert!(plan_launch_command(&request) - .unwrap() - .argument_file - .is_some()); - } - - #[test] - fn launch_plan_prefers_an_explicit_feature_version() { - let mut request = request("/jdk/bin/java", &["-cp", "a.jar", "Main"]); - request.java_feature_version = Some(8); - request.java_release_contents = Some(vec!["JAVA_VERSION=\"17\"".to_owned()]); - assert_eq!(plan_launch_command(&request).unwrap(), unchanged(&request)); - } - - #[test] - fn launch_plan_command_dispatches_through_the_protocol() { - let response: serde_json::Value = serde_json::from_str(&crate::execute_json( - &serde_json::json!({ - "id": "plan", - "command": "execution.planLaunchCommand", - "payload": { - "executable": "/jdk/bin/java", - "arguments": ["-cp", "a.jar", "Main"], - "javaFeatureVersion": 17, - "commandLineLimit": 10 - } - }) - .to_string(), - )) - .unwrap(); - assert_eq!(response["ok"], true, "{response}"); - assert_eq!(response["data"]["argumentFile"]["argumentIndex"], 0); - assert_eq!( - response["data"]["arguments"], - serde_json::json!(["", "Main"]) - ); - } - - #[test] - fn launch_plan_rejects_an_empty_executable() { - let request = request("", &[]); - assert!(plan_launch_command(&request).is_err()); - } -} diff --git a/rust/lithe-core/src/execution/mod.rs b/rust/lithe-core/src/execution/mod.rs index af5f01ff..850d31ef 100644 --- a/rust/lithe-core/src/execution/mod.rs +++ b/rust/lithe-core/src/execution/mod.rs @@ -30,9 +30,3 @@ pub use environment::{ resolve_launch_environment, resolve_search_path, LaunchEnvironmentRequest, LaunchEnvironmentResponse, SearchPathCandidate, SearchPathRequest, }; - -mod launch_plan; -pub use launch_plan::{ - java_feature_version_from_release, plan_launch_command, LaunchArgumentFile, LaunchCommandPlan, - LaunchCommandRequest, -}; diff --git a/rust/lithe-core/src/lsp/interface/client.rs b/rust/lithe-core/src/lsp/interface/client.rs index 2b2195de..ebbef4b5 100644 --- a/rust/lithe-core/src/lsp/interface/client.rs +++ b/rust/lithe-core/src/lsp/interface/client.rs @@ -44,8 +44,8 @@ pub(super) fn client_initialize_for_plugin( "completionItem": { "snippetSupport": true, "documentationFormat": ["markdown", "plaintext"], - // Intelephense and JDT LS omit typed class/namespace labels - // unless the client advertises labelDetailsSupport. + // Some servers (Intelephense, for one) omit typed class and + // namespace labels unless the client advertises labelDetailsSupport. "labelDetailsSupport": true, "resolveSupport": { "properties": [ @@ -1512,7 +1512,7 @@ fn parse_completion_item(item: &Value) -> Option { { mapped["additionalTextEdits"] = json!(edits); } - // `data: null` makes Intelephense/JDT LS treat resolve as a no-op, so only + // `data: null` makes some servers treat resolve as a no-op, so only // forward a real resolve token. Namespace display lives in labelDetails; // import `use`/`import` edits usually arrive only from completionItem/resolve. if let Some(data) = item.get("data").filter(|value| !value.is_null()) { @@ -2404,7 +2404,7 @@ mod semantic_tests { let mut state = LspClientState::default(); apply_dynamic_registration( &mut state, - &json!({"params": {"registrations": [{"id": "java", "method": "textDocument/semanticTokens", "registerOptions": options()}]}}), + &json!({"params": {"registrations": [{"id": "semantic", "method": "textDocument/semanticTokens", "registerOptions": options()}]}}), ); assert!(state .server_capabilities @@ -2412,7 +2412,7 @@ mod semantic_tests { let id = allocate_request(&mut state, "textDocument/semanticTokens/full"); apply_dynamic_unregistration( &mut state, - &json!({"params": {"unregisterations": [{"id": "java", "method": "textDocument/semanticTokens"}]}}), + &json!({"params": {"unregisterations": [{"id": "semantic", "method": "textDocument/semanticTokens"}]}}), ); assert!(!state .server_capabilities diff --git a/rust/lithe-core/src/lsp/interface/engine.rs b/rust/lithe-core/src/lsp/interface/engine.rs index 132a2737..6d833b5a 100644 --- a/rust/lithe-core/src/lsp/interface/engine.rs +++ b/rust/lithe-core/src/lsp/interface/engine.rs @@ -8289,9 +8289,9 @@ mod tests { } } - // Regression: JDT publishes diagnostics under the resolved path of a file - // the host opened through a symbolic link; matching the URIs textually - // dropped every Java error in such a workspace. + // Regression: a server publishes diagnostics under the resolved path of a + // file the host opened through a symbolic link; matching the URIs textually + // dropped every error in such a workspace. #[cfg(unix)] #[test] fn diagnostics_published_under_the_resolved_path_reach_the_linked_document() { @@ -8977,7 +8977,7 @@ mod tests { fn a_negotiated_build_request_forwards_the_plan_and_returns_the_reply() { let mut harness = negotiated_build_provider(); let params = json!({ - "buildSystemID": "gradle", + "buildSystemID": "make", "root": "", "options": { "offline": true }, "optionArguments": ["--offline"], @@ -9608,7 +9608,7 @@ mod tests { // deadline, late reply, revocation, and compatibility with a backend that // only speaks the build protocol) is driven at a chosen moment. - const FUNCTION_PLUGIN: &str = "dev.lithe.fixture.maven"; + const FUNCTION_PLUGIN: &str = "dev.lithe.fixture.build"; /// A session owned by `FUNCTION_PLUGIN` whose backend answered /// `initialize` with `experimental`. @@ -9782,7 +9782,7 @@ mod tests { // Another plugin's reference never reaches this process. let error = invoke( &harness, - "dev.lithe.fixture.gradle", + "dev.lithe.fixture.other-build", "snapshot.refresh", None, ) @@ -9846,7 +9846,7 @@ mod tests { harness.server.send(json!({ "jsonrpc": "2.0", "id": request_id, - "error": { "code": -32000, "message": "Snapshot is locked.", "data": { "code": "maven.snapshotLocked" } } + "error": { "code": -32000, "message": "Snapshot is locked.", "data": { "code": "build.snapshotLocked" } } })); let error = harness .await_event(|event| event.operation_id.as_deref() == Some(operation.as_str())) @@ -9857,7 +9857,7 @@ mod tests { assert_eq!(error.message, "Snapshot is locked."); assert_eq!( error.plugin_error_code.as_deref(), - Some("maven.snapshotLocked") + Some("build.snapshotLocked") ); } diff --git a/rust/lithe-core/src/lsp/interface/process.rs b/rust/lithe-core/src/lsp/interface/process.rs index dc9ee61a..6c8889c2 100644 --- a/rust/lithe-core/src/lsp/interface/process.rs +++ b/rust/lithe-core/src/lsp/interface/process.rs @@ -7,7 +7,7 @@ //! against a scripted server instead of a real one. //! //! On Windows a server and everything it starts share one Job Object, so a -//! batch launcher's JVM ends with it and none outlives a crashed host. +//! runtime a batch launcher starts ends with it and none outlives a crashed host. use crate::protocol::{CoreError, ErrorCode}; use std::collections::BTreeMap; @@ -385,7 +385,7 @@ mod tests { #[test] fn batch_language_server_uses_cmd_exe() { let spec = LspProcessSpec { - executable: PathBuf::from(r"C:\Program Files\Lithe\jdtls.bat"), + executable: PathBuf::from(r"C:\Program Files\Lithe\language-server.bat"), arguments: vec!["-data".to_string(), r"C:\workspace data".to_string()], working_directory: PathBuf::from(r"C:\workspace"), environment: BTreeMap::new(), @@ -434,7 +434,7 @@ mod tests { /// Not a scenario of its own: a no-op unless the termination test launches /// this test binary with it. It then stays alive until its inherited stdin - /// closes, like the JVM a batch language-server launcher starts. + /// closes, like the runtime a batch language-server launcher starts. #[test] fn stdin_hold_helper() { if std::env::var_os(STDIN_HOLD_ENV).is_none() { @@ -529,7 +529,7 @@ mod tests { let executable = root.join("scripted server.cmd"); let echo_output = root.join("arguments.txt"); // The script forwards `%*` to a native Windows program, which parses the - // command line the way a real language server such as java.exe does. + // command line the way a real native language server does. // This test binary plays that program: starting PowerShell here instead // took from 0.2 s to the whole per-test budget on CI runners. fs::write( diff --git a/rust/lithe-core/src/lsp/languages/catalog.rs b/rust/lithe-core/src/lsp/languages/catalog.rs deleted file mode 100644 index 4c6c522a..00000000 --- a/rust/lithe-core/src/lsp/languages/catalog.rs +++ /dev/null @@ -1,392 +0,0 @@ -//! Loading and validation for built-in and workspace language-provider catalogs. -//! -//! Note: LSP runtime ownership and provider-routing invariants are recorded in -//! .agents/notes/implemented/architecture/2026-09-13-language-tooling-and-lsp-runtime-ownership.md -//! -//! Checklist for adding a new language server: -//! 1. Define a stable `id`, file-match rules, `languageId`, candidate executable -//! names and arguments; declare side-effect-free `validationArguments` for -//! any shim/proxy tool. -//! 2. Define `languageServerInstallation` only if an install entry point is -//! needed; do not add a provider-ID branch in Swift UI. -//! 3. Confirm the server supports stdio with standard `Content-Length` framing. -//! 4. Do not branch by language in UI or manager code; server differences -//! belong in the descriptor or a dedicated adapter. -//! 5. Verify capability with the `initialize` response; never treat the -//! catalog's `languageServer` flag as proof of feature support. -//! 6. Test at minimum: initialize timeout/error, didOpen/change/close, request -//! timeout/late response, shutdown/exit, forced stop, and abnormal exit. -//! 7. Cover malformed/partial/multiple frames, dynamic capability changes, -//! stale diagnostics, UTF-16 positions, file URIs with spaces/non-ASCII -//! characters, and servers that emit output immediately on launch. - -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::collections::BTreeMap; -use std::path::{Path, PathBuf}; - -const BUILTIN_LANGUAGE_PROVIDERS: &str = include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/resources/lsp/language-providers.json" -)); - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -/// Effective provider catalog after applying an optional workspace override. -pub struct LspProviderCatalog { - pub version: u32, - pub origin: LspProviderCatalogOrigin, - pub providers: Vec, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub diagnostics: Vec, -} - -#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize)] -#[serde(rename_all = "camelCase")] -/// Source that last changed the effective provider configuration. -pub enum LspProviderCatalogOrigin { - /// The effective catalog is exactly the embedded provider document. - Builtin, - /// At least one workspace-local patch was merged into the built-ins. - WorkspaceOverride, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -/// Non-fatal configuration problem surfaced alongside usable providers. -pub struct LspProviderConfigDiagnostic { - pub path: String, - pub message: String, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -/// File recognition, capabilities, and server metadata for one language provider. -pub struct LspProviderDescriptor { - pub id: String, - pub display_name: String, - pub file_extensions: Vec, - pub file_names: Vec, - pub file_name_prefixes: Vec, - pub capabilities: Vec, - pub activation_policy: LspActivationPolicy, - pub language_id: Option, - pub language_ids_by_extension: BTreeMap, - pub language_ids_by_file_name: BTreeMap, - pub language_server_launch: Option, - pub language_server_installation: Option, -} - -#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Serialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -/// Executable candidates and initialization values for a language server. -pub struct LspServerLaunchDescriptor { - pub executable_names: Vec, - #[serde(default)] - pub arguments: Vec, - #[serde(default)] - pub validation_arguments: Vec, - #[serde(default)] - pub environment: BTreeMap, - #[serde(default)] - pub initialization_options: Option, -} - -#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Serialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -/// Installation hints shown when no compatible server executable is available. -pub struct LspServerInstallationDescriptor { - #[serde(default)] - pub homebrew_formula: Option, - #[serde(default, rename = "officialDownloadURL")] - pub official_download_url: Option, -} - -#[derive(Debug, Clone, Copy, Deserialize, Eq, PartialEq, Serialize)] -#[serde(rename_all = "camelCase")] -/// Product feature that a language provider can contribute. -pub enum LspProviderCapability { - /// Produces runnable project configurations. - Run, - /// Supplies semantic language features through an LSP process. - LanguageServer, - /// Supplies an adapter that implements the Debug Adapter Protocol. - DebugAdapter, - /// Formats source documents. - Formatting, - /// Discovers or runs project tests. - Testing, -} - -#[derive(Debug, Clone, Copy, Deserialize, Eq, PartialEq, Serialize)] -#[serde(rename_all = "camelCase")] -/// Policy controlling when a provider's external process may start. -pub enum LspActivationPolicy { - /// Starts the provider only after a feature explicitly requests it. - OnDemand, - /// Starts the provider as soon as its project activation conditions match. - Always, -} - -impl Default for LspActivationPolicy { - fn default() -> Self { - Self::OnDemand - } -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct LspProviderConfigDocument { - #[serde(default, rename = "$schema")] - _schema: Option, - #[serde(default = "default_config_version")] - version: u32, - #[serde(default)] - providers: Vec, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct LspProviderPatch { - id: String, - #[serde(default)] - display_name: Option, - #[serde(default)] - file_extensions: Option>, - #[serde(default)] - file_names: Option>, - #[serde(default)] - file_name_prefixes: Option>, - #[serde(default)] - capabilities: Option>, - #[serde(default)] - activation_policy: Option, - #[serde(default)] - language_id: Option, - #[serde(default)] - language_ids_by_extension: Option>, - #[serde(default)] - language_ids_by_file_name: Option>, - #[serde(default)] - language_server_launch: Option, - #[serde(default)] - language_server_installation: Option, - #[serde(default)] - disabled: bool, -} -/// Serializes the effective provider catalog for the C ABI. -pub fn provider_catalog_json(workspace_root: Option<&Path>) -> String { - let catalog = provider_catalog(workspace_root); - serde_json::to_string(&catalog).unwrap_or_else(|_| { - concat!( - "{\"version\":1,\"origin\":\"builtin\",\"providers\":[],", - "\"diagnostics\":[{\"path\":\"builtin:lsp\",", - "\"message\":\"Could not serialize the language-provider catalog.\"}]}" - ) - .to_string() - }) -} - -/// Loads built-ins and applies a workspace-local provider configuration, if present. -pub fn provider_catalog(workspace_root: Option<&Path>) -> LspProviderCatalog { - let mut diagnostics = Vec::new(); - let mut origin = LspProviderCatalogOrigin::Builtin; - let mut document = match parse_document(BUILTIN_LANGUAGE_PROVIDERS, "builtin:lsp") { - Ok(document) => document, - Err(message) => { - diagnostics.push(LspProviderConfigDiagnostic { - path: "builtin:lsp".to_string(), - message, - }); - LspProviderConfigDocument { - _schema: None, - version: 1, - providers: Vec::new(), - } - } - }; - - if let Some(root) = workspace_root { - let path = project_config_path(root); - if path.is_file() { - match std::fs::read_to_string(&path) { - Ok(raw) => match parse_document(&raw, &path.display().to_string()) { - Ok(project_document) => { - document = merge_documents(document, project_document); - origin = LspProviderCatalogOrigin::WorkspaceOverride; - } - Err(message) => diagnostics.push(LspProviderConfigDiagnostic { - path: path.display().to_string(), - message, - }), - }, - Err(error) => diagnostics.push(LspProviderConfigDiagnostic { - path: path.display().to_string(), - message: error.to_string(), - }), - } - } - } - - let mut providers = Vec::new(); - for patch in document.providers { - if patch.disabled { - continue; - } - providers.push(LspProviderDescriptor::from_patch(patch)); - } - LspProviderCatalog { - version: document.version, - origin, - providers, - diagnostics, - } -} - -fn parse_document(raw: &str, source: &str) -> Result { - serde_json::from_str(raw).map_err(|error| format!("{source}: {error}")) -} - -fn merge_documents( - mut base: LspProviderConfigDocument, - project: LspProviderConfigDocument, -) -> LspProviderConfigDocument { - base.version = project.version.max(base.version); - for patch in project.providers { - if let Some(existing) = base - .providers - .iter_mut() - .find(|provider| provider.id == patch.id) - { - existing.apply(patch); - } else { - base.providers.push(patch); - } - } - base -} - -fn project_config_path(root: &Path) -> PathBuf { - root.join(".lithe") - .join("lsp") - .join("language-providers.json") -} - -fn default_config_version() -> u32 { - 1 -} - -impl LspProviderPatch { - fn apply(&mut self, patch: LspProviderPatch) { - if patch.display_name.is_some() { - self.display_name = patch.display_name; - } - if patch.file_extensions.is_some() { - self.file_extensions = patch.file_extensions; - } - if patch.file_names.is_some() { - self.file_names = patch.file_names; - } - if patch.file_name_prefixes.is_some() { - self.file_name_prefixes = patch.file_name_prefixes; - } - if patch.capabilities.is_some() { - self.capabilities = patch.capabilities; - } - if patch.activation_policy.is_some() { - self.activation_policy = patch.activation_policy; - } - if patch.language_id.is_some() { - self.language_id = patch.language_id; - } - if patch.language_ids_by_extension.is_some() { - self.language_ids_by_extension = patch.language_ids_by_extension; - } - if patch.language_ids_by_file_name.is_some() { - self.language_ids_by_file_name = patch.language_ids_by_file_name; - } - if patch.language_server_launch.is_some() { - self.language_server_launch = patch.language_server_launch; - } - if patch.language_server_installation.is_some() { - self.language_server_installation = patch.language_server_installation; - } - self.disabled = patch.disabled; - } -} - -impl LspProviderDescriptor { - fn from_patch(patch: LspProviderPatch) -> Self { - let id = normalized_id(&patch.id); - let display_name = patch - .display_name - .filter(|value| !value.trim().is_empty()) - .unwrap_or_else(|| id.clone()); - let capabilities = patch.capabilities.unwrap_or_else(|| { - vec![ - LspProviderCapability::LanguageServer, - LspProviderCapability::Formatting, - ] - }); - Self { - id: id.clone(), - display_name, - file_extensions: normalized_values(patch.file_extensions.unwrap_or_default(), true), - file_names: normalized_values(patch.file_names.unwrap_or_default(), false), - file_name_prefixes: normalized_values( - patch.file_name_prefixes.unwrap_or_default(), - false, - ), - capabilities, - activation_policy: patch.activation_policy.unwrap_or_default(), - language_id: patch.language_id.filter(|value| !value.trim().is_empty()), - language_ids_by_extension: normalized_map( - patch.language_ids_by_extension.unwrap_or_default(), - true, - ), - language_ids_by_file_name: normalized_map( - patch.language_ids_by_file_name.unwrap_or_default(), - false, - ), - language_server_launch: patch.language_server_launch, - language_server_installation: patch.language_server_installation, - } - } -} - -fn normalized_id(value: &str) -> String { - value.trim().to_ascii_lowercase() -} - -fn normalized_values(values: Vec, trim_dot: bool) -> Vec { - let mut result = Vec::new(); - for value in values { - let normalized = normalized_key(&value, trim_dot); - if !normalized.is_empty() && !result.contains(&normalized) { - result.push(normalized); - } - } - result -} - -fn normalized_map(values: BTreeMap, trim_dot: bool) -> BTreeMap { - values - .into_iter() - .filter_map(|(key, value)| { - let key = normalized_key(&key, trim_dot); - if key.is_empty() || value.trim().is_empty() { - None - } else { - Some((key, value)) - } - }) - .collect() -} - -fn normalized_key(value: &str, trim_dot: bool) -> String { - let mut value = value.trim().to_ascii_lowercase(); - if trim_dot { - value = value.trim_start_matches('.').to_string(); - } - value -} diff --git a/rust/lithe-core/src/lsp/languages/mod.rs b/rust/lithe-core/src/lsp/languages/mod.rs index aed65c66..f69c0402 100644 --- a/rust/lithe-core/src/lsp/languages/mod.rs +++ b/rust/lithe-core/src/lsp/languages/mod.rs @@ -1,8 +1,5 @@ //! Dynamic provider metadata and host-model adapters for individual languages. -mod catalog; pub(crate) mod indexing; #[cfg(test)] pub(crate) mod swift; - -pub(crate) use catalog::*; diff --git a/rust/lithe-core/src/lsp/mod.rs b/rust/lithe-core/src/lsp/mod.rs index ea2e495a..9e44b41e 100644 --- a/rust/lithe-core/src/lsp/mod.rs +++ b/rust/lithe-core/src/lsp/mod.rs @@ -11,7 +11,6 @@ pub(crate) mod workspace_edit_plan; mod workspace_scope; pub(crate) use interface::*; -pub(crate) use languages::*; pub(crate) use lightweight::*; #[cfg(test)] diff --git a/rust/lithe-core/src/lsp/tests.rs b/rust/lithe-core/src/lsp/tests.rs index b4b3329b..6ab260ee 100644 --- a/rust/lithe-core/src/lsp/tests.rs +++ b/rust/lithe-core/src/lsp/tests.rs @@ -1,258 +1,6 @@ use super::*; use crate::protocol::ErrorCode; use serde_json::{json, Value}; -use std::fs; -use std::path::PathBuf; -use std::time::{SystemTime, UNIX_EPOCH}; - -fn temporary_root(label: &str) -> PathBuf { - let nonce = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system clock should be valid") - .as_nanos(); - std::env::temp_dir().join(format!("lithe-lsp-{label}-{}-{nonce}", std::process::id())) -} - -#[test] -fn builtin_catalog_describes_market_lsp_providers() { - let catalog = provider_catalog(None); - let ids: Vec<_> = catalog - .providers - .iter() - .map(|provider| provider.id.as_str()) - .collect(); - assert!(ids.starts_with(&["java", "go", "python", "node", "rust"])); - assert!(ids.contains(&"swift")); - assert!(ids.contains(&"clangd")); - assert!(ids.contains(&"dockerfile")); - assert!(ids.contains(&"graphql")); - let clangd = catalog - .providers - .iter() - .find(|provider| provider.id == "clangd") - .expect("clangd provider should exist"); - assert_eq!( - clangd.language_ids_by_extension.get("m"), - Some(&"objective-c".to_string()) - ); - let swift = catalog - .providers - .iter() - .find(|provider| provider.id == "swift") - .expect("swift provider should exist"); - let swift_launch = swift - .language_server_launch - .as_ref() - .expect("swift launch descriptor should exist"); - assert_eq!( - swift_launch.executable_names, - vec!["sourcekit-lsp".to_string()] - ); - let go = catalog - .providers - .iter() - .find(|provider| provider.id == "go") - .expect("go provider should exist"); - let go_installation = go - .language_server_installation - .as_ref() - .expect("go installation descriptor should exist"); - assert_eq!(go_installation.homebrew_formula.as_deref(), Some("gopls")); - assert_eq!( - go_installation.official_download_url.as_deref(), - Some("https://go.dev/gopls/") - ); - let rust = catalog - .providers - .iter() - .find(|provider| provider.id == "rust") - .expect("rust provider should exist"); - assert_eq!( - rust.language_server_launch - .as_ref() - .expect("rust launch descriptor should exist") - .validation_arguments, - vec!["--version".to_string()] - ); - // Intelephense only speaks LSP over stdio, so a bare launch would exit - // instead of serving the editor. The catalog is the only truth for the - // macOS language server settings, so the arguments are asserted here. - let php = catalog - .providers - .iter() - .find(|provider| provider.id == "php") - .expect("php provider should exist"); - assert_eq!( - php.capabilities, - vec![ - LspProviderCapability::Run, - LspProviderCapability::LanguageServer, - LspProviderCapability::Formatting, - LspProviderCapability::Testing, - ] - ); - let php_launch = php - .language_server_launch - .as_ref() - .expect("php launch descriptor should exist"); - assert_eq!( - php_launch.executable_names, - vec!["intelephense".to_string()] - ); - assert_eq!(php_launch.arguments, vec!["--stdio".to_string()]); -} - -#[test] -fn project_config_extends_and_overrides_builtin_catalog() { - let root = temporary_root("project-config"); - fs::create_dir_all(root.join(".lithe").join("lsp")).unwrap(); - fs::write( - root.join(".lithe") - .join("lsp") - .join("language-providers.json"), - r#"{ - "version": 1, - "providers": [ - { - "id": "roc", - "displayName": "Roc", - "fileExtensions": ["roc"], - "capabilities": ["languageServer", "formatting"], - "activationPolicy": "onDemand", - "languageId": "roc" - }, - { - "id": "swift", - "fileExtensions": ["swift", "swiftinterface"], - "languageServerLaunch": { - "executableNames": ["custom-sourcekit-lsp"], - "arguments": ["--stdio"], - "environment": { - "SOURCEKIT_TOOLCHAIN": "custom" - }, - "initializationOptions": { - "indexing": true - } - }, - "languageServerInstallation": { - "homebrewFormula": "custom-sourcekit-lsp", - "officialDownloadURL": "https://example.com/sourcekit-lsp" - } - }, - { - "id": "perl", - "disabled": true - } - ] - }"#, - ) - .unwrap(); - - let catalog = provider_catalog(Some(&root)); - assert_eq!(catalog.origin, LspProviderCatalogOrigin::WorkspaceOverride); - assert!(catalog - .providers - .iter() - .any(|provider| provider.id == "roc")); - let swift = catalog - .providers - .iter() - .find(|provider| provider.id == "swift") - .expect("swift provider should still exist"); - assert!(swift - .file_extensions - .contains(&"swiftinterface".to_string())); - let swift_launch = swift - .language_server_launch - .as_ref() - .expect("swift launch descriptor should be overridden"); - assert_eq!( - swift_launch.executable_names, - vec!["custom-sourcekit-lsp".to_string()] - ); - assert_eq!(swift_launch.arguments, vec!["--stdio".to_string()]); - assert_eq!( - swift_launch.environment.get("SOURCEKIT_TOOLCHAIN"), - Some(&"custom".to_string()) - ); - assert_eq!( - swift_launch.initialization_options, - Some(json!({ "indexing": true })) - ); - let swift_installation = swift - .language_server_installation - .as_ref() - .expect("swift installation descriptor should be overridden"); - assert_eq!( - swift_installation.homebrew_formula.as_deref(), - Some("custom-sourcekit-lsp") - ); - assert!(!catalog - .providers - .iter() - .any(|provider| provider.id == "perl")); - - fs::remove_dir_all(root).unwrap(); -} - -#[test] -fn ffi_json_is_a_standalone_catalog_document() { - let raw = provider_catalog_json(None); - let value: Value = serde_json::from_str(&raw).expect("catalog should be JSON"); - assert_eq!(value["version"], 2); - assert_eq!(value["origin"], "builtin"); - assert!(value["providers"].as_array().unwrap().len() > 10); - assert!(value.get("ok").is_none()); - assert!(value.get("command").is_none()); -} - -#[test] -fn project_catalog_reports_unknown_configuration_fields() { - let root = temporary_root("project-config-unknown-field"); - fs::create_dir_all(root.join(".lithe").join("lsp")).unwrap(); - fs::write( - root.join(".lithe") - .join("lsp") - .join("language-providers.json"), - r#"{ - "version": 2, - "providers": [{ "id": "go", "languageServerLanch": {} }] - }"#, - ) - .unwrap(); - - let catalog = provider_catalog(Some(&root)); - assert_eq!(catalog.origin, LspProviderCatalogOrigin::Builtin); - assert_eq!(catalog.diagnostics.len(), 1); - assert!(catalog.diagnostics[0].message.contains("unknown field")); - assert!(catalog.providers.iter().any(|provider| provider.id == "go")); - - fs::remove_dir_all(root).unwrap(); -} - -#[test] -fn malformed_project_catalog_is_rejected_with_a_visible_diagnostic() { - let root = temporary_root("project-config-malformed"); - let config_path = root - .join(".lithe") - .join("lsp") - .join("language-providers.json"); - fs::create_dir_all(config_path.parent().unwrap()).unwrap(); - fs::write(&config_path, r#"{"version":2,"providers":["#).unwrap(); - - let catalog = provider_catalog(Some(&root)); - - assert_eq!(catalog.origin, LspProviderCatalogOrigin::Builtin); - assert!(catalog.providers.iter().any(|provider| provider.id == "go")); - assert_eq!(catalog.diagnostics.len(), 1); - assert_eq!( - catalog.diagnostics[0].path, - config_path.display().to_string() - ); - assert!(catalog.diagnostics[0].message.contains("EOF")); - - fs::remove_dir_all(root).unwrap(); -} #[test] fn text_edits_use_lsp_utf16_positions() { diff --git a/rust/lithe-core/src/plugins/declarative.rs b/rust/lithe-core/src/plugins/declarative.rs index b6d830b4..6dcd03e6 100644 --- a/rust/lithe-core/src/plugins/declarative.rs +++ b/rust/lithe-core/src/plugins/declarative.rs @@ -2446,8 +2446,8 @@ impl ServerProcess { .unwrap_or(language_server_sessions::RESOLVE_TIMEOUT_MILLISECONDS), }), }; - // Initialization options can name code the server loads (JDT LS - // bundles, for example), so changing them is a new program too. + // Initialization options can name code the server loads (bundles or + // extensions, for example), so changing them is a new program too. if let Some(options) = &self.initialization_options { program["initializationOptions"] = options.clone(); } diff --git a/rust/lithe-core/src/plugins/web_content_tests.rs b/rust/lithe-core/src/plugins/web_content_tests.rs index 19c60777..c2e80df1 100644 --- a/rust/lithe-core/src/plugins/web_content_tests.rs +++ b/rust/lithe-core/src/plugins/web_content_tests.rs @@ -382,8 +382,8 @@ fn build_methods_pass_to_a_host_that_serves_them() { page, 2, "functions.list", - json!({ "instance": "maven:root" }), + json!({ "instance": "build:root" }), ); - assert_eq!(with_instance["action"]["params"]["instance"], "maven:root"); + assert_eq!(with_instance["action"]["params"]["instance"], "build:root"); close_command(json!({ "page": page })).expect("close"); } diff --git a/rust/lithe-core/src/project_generators/archive.rs b/rust/lithe-core/src/project_generators/archive.rs index d477cc40..6180bdfa 100644 --- a/rust/lithe-core/src/project_generators/archive.rs +++ b/rust/lithe-core/src/project_generators/archive.rs @@ -358,13 +358,13 @@ mod tests { let (directory, path) = archive(&[ ("pom.xml", b"", None), ("mvnw", b"#!/bin/sh", Some(0o755)), - ("src/main/java/App.java", b"class App {}", None), + ("src/main/app/App.src", b"class App {}", None), ]); let destination = directory.path().join("project"); fs::create_dir(&destination).unwrap(); assert_eq!(extract(&path, &destination).unwrap(), 3); assert_eq!( - fs::read_to_string(destination.join("src/main/java/App.java")).unwrap(), + fs::read_to_string(destination.join("src/main/app/App.src")).unwrap(), "class App {}" ); #[cfg(unix)] diff --git a/rust/lithe-core/src/project_generators/form.rs b/rust/lithe-core/src/project_generators/form.rs index f5280dbc..8c7e941d 100644 --- a/rust/lithe-core/src/project_generators/form.rs +++ b/rust/lithe-core/src/project_generators/form.rs @@ -371,7 +371,7 @@ fn tool_choices(field: &GeneratorField, input: &FormInput) -> Vec { } /// Compares a value with the field's `maximumVersion`, expanded over the -/// earlier fields. An empty value or bound (no JDK chosen yet) is left to the +/// earlier fields. An empty value or bound (no tool chosen yet) is left to the /// fields' own checks. fn version_too_high( field: &GeneratorField, @@ -493,9 +493,9 @@ mod tests { fn hidden_fields_are_not_validated_or_read() { let generator = generator(json!([ {"id": "buildSystem", "type": "enum", "title": "Build", "options": [ - {"value": "none", "title": "None"}, {"value": "maven", "title": "Maven"}]}, + {"value": "none", "title": "None"}, {"value": "packaged", "title": "Packaged"}]}, {"id": "groupId", "type": "text", "title": "Group", - "visibleWhen": {"field": "buildSystem", "equals": "maven"}} + "visibleWhen": {"field": "buildSystem", "equals": "packaged"}} ])); let state = evaluate(&generator, &input("demo", json!({}), &[])); assert!(!field(&state, "groupId").visible); @@ -504,7 +504,7 @@ mod tests { let state = evaluate( &generator, - &input("demo", json!({"buildSystem": "maven"}), &[]), + &input("demo", json!({"buildSystem": "packaged"}), &[]), ); assert_eq!( field(&state, "groupId").problem, @@ -516,31 +516,34 @@ mod tests { fn rejects_unsafe_text_and_old_tools() { let generator = generator(json!([ {"id": "groupId", "type": "text", "title": "Group", "pattern": "[a-z.]+"}, - {"id": "jdk", "type": "tool", "title": "JDK", "tool": "java", "minimumVersion": "17"} + {"id": "compiler", "type": "tool", "title": "Compiler", "tool": "compiler", "minimumVersion": "17"} ])); let mut form = input("demo", json!({"groupId": "-Dx=y"}), &["groupId"]); form.tools - .insert("jdk".into(), vec![installation("/jdk", "11.0.2")]); + .insert("compiler".into(), vec![installation("/compiler", "11.0.2")]); let state = evaluate(&generator, &form); assert_eq!( field(&state, "groupId").problem, Some(FieldProblem::InvalidFormat) ); - assert_eq!(field(&state, "jdk").problem, Some(FieldProblem::ToolTooOld)); + assert_eq!( + field(&state, "compiler").problem, + Some(FieldProblem::ToolTooOld) + ); } - /// A target release newer than the selected JDK cannot compile, so the - /// bound follows the JDK; before a JDK is chosen nothing is compared. + /// A target level newer than the selected compiler cannot compile, so the + /// bound follows the compiler; before one is chosen nothing is compared. #[test] fn maximum_version_follows_an_earlier_tool() { let generator = generator(json!([ - {"id": "jdk", "type": "tool", "title": "JDK", "tool": "java", "minimumVersion": "17"}, - {"id": "javaVersion", "type": "enum", "title": "Java", "maximumVersion": "${field.jdk.major}", + {"id": "compiler", "type": "tool", "title": "Compiler", "tool": "compiler", "minimumVersion": "17"}, + {"id": "languageLevel", "type": "enum", "title": "Level", "maximumVersion": "${field.compiler.major}", "options": [{"value": "17", "title": "17"}, {"value": "21", "title": "21"}]} ])); - let mut form = input("demo", json!({"javaVersion": "21"}), &[]); + let mut form = input("demo", json!({"languageLevel": "21"}), &[]); assert_eq!( - field(&evaluate(&generator, &form), "javaVersion").problem, + field(&evaluate(&generator, &form), "languageLevel").problem, None ); for (version, problem) in [ @@ -549,9 +552,9 @@ mod tests { ("25", None), ] { form.tools - .insert("jdk".into(), vec![installation("/jdk", version)]); + .insert("compiler".into(), vec![installation("/compiler", version)]); assert_eq!( - field(&evaluate(&generator, &form), "javaVersion").problem, + field(&evaluate(&generator, &form), "languageLevel").problem, problem, "{version}" ); diff --git a/rust/lithe-core/src/project_generators/manifest.rs b/rust/lithe-core/src/project_generators/manifest.rs index 0148b6e1..e8830c61 100644 --- a/rust/lithe-core/src/project_generators/manifest.rs +++ b/rust/lithe-core/src/project_generators/manifest.rs @@ -304,8 +304,8 @@ pub(crate) enum GeneratorStep { arguments: Vec, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] environment: BTreeMap, - /// The tool writes the project into this subdirectory (Maven's - /// archetype plugin uses the artifact ID); its contents become the + /// The tool writes the project into this subdirectory (a scaffolding + /// tool may name it after the project); its contents become the /// project root. #[serde(default, skip_serializing_if = "Option::is_none")] collect: Option, diff --git a/rust/lithe-core/src/project_generators/mod.rs b/rust/lithe-core/src/project_generators/mod.rs index 9e77afb0..968b80f6 100644 --- a/rust/lithe-core/src/project_generators/mod.rs +++ b/rust/lithe-core/src/project_generators/mod.rs @@ -5,8 +5,8 @@ //! A plugin describes a way to start a project as data (`projectGenerators` //! in `plugin.json`): where it is listed, which fields the user fills in, //! which plugins the result needs, and the steps that produce it — template -//! files from the package, a scaffolding command such as `mvn -//! archetype:generate`, or a zip archive such as Spring Initializr's. Core +//! files from the package, a scaffolding command the plugin names, or a zip +//! archive downloaded from a project starter service. Core //! validates the declaration, evaluates the form (derived defaults, //! visibility, errors), and turns a filled form into a plan: expanded files, //! argv, and URLs. Hosts own the window, resolve executables, download, run diff --git a/rust/lithe-core/src/project_generators/template.rs b/rust/lithe-core/src/project_generators/template.rs index f871805c..ca98af17 100644 --- a/rust/lithe-core/src/project_generators/template.rs +++ b/rust/lithe-core/src/project_generators/template.rs @@ -1,8 +1,8 @@ //! Variables of project generator templates. //! //! Strings in the manifest use the `${...}` syntax every plugin template -//! uses. Template files in the package use `{{ ... }}` instead, because Maven -//! POMs, Gradle scripts, and Spring properties already contain `${...}` that +//! uses. Template files in the package use `{{ ... }}` instead, because build +//! scripts and property files of many tools already contain `${...}` that //! must reach the project unchanged. In files, only `{{` followed by //! `project.` or `field.` is a variable; any other `{{` is copied as text. @@ -31,7 +31,7 @@ pub(crate) enum Source { pub(crate) enum Modifier { /// Dots become `/`: a package name as a source directory. Path, - /// A valid Java-style package: lowercase, only letters, digits, + /// A dotted package identifier: lowercase, only letters, digits, /// underscores, and single dots, each segment starting with a letter or /// underscore. `com.example.my-app` becomes `com.example.myapp`. Package, @@ -204,16 +204,17 @@ fn apply(modifier: Modifier, value: &str) -> String { } } -/// `1.8.0_412` → `8`, `21.0.2` → `21`, `25` → `25`. +/// The first numeric component: `21.0.2` → `21`, `25` → `25`. +/// +/// Tool-specific numbering (such as a legacy `1.` prefix) is normalized by the +/// tool definition's `version.pattern` capture before the version reaches a +/// template, so this reader never interprets a tool's scheme. pub(crate) fn major_version(version: &str) -> String { - let mut numbers = version + version .split(|character: char| !character.is_ascii_digit()) - .filter(|part| !part.is_empty()); - match (numbers.next(), numbers.next()) { - (Some("1"), Some(minor)) => minor.to_string(), - (Some(major), _) => major.to_string(), - _ => String::new(), - } + .find(|part| !part.is_empty()) + .unwrap_or_default() + .to_string() } fn package_name(value: &str) -> String { @@ -259,18 +260,18 @@ mod tests { }) ); assert_eq!( - Variable::parse("field.jdk.major").map(|variable| variable.source), - Some(Source::ToolMajor("jdk".into())) + Variable::parse("field.compiler.major").map(|variable| variable.source), + Some(Source::ToolMajor("compiler".into())) ); assert_eq!( - Variable::parse("field.jdk.version").map(|variable| variable.source), - Some(Source::ToolVersion("jdk".into())) + Variable::parse("field.compiler.version").map(|variable| variable.source), + Some(Source::ToolVersion("compiler".into())) ); assert_eq!( Variable::parse("field.python.executable").map(|variable| variable.source), Some(Source::ToolExecutable("python".into())) ); - assert!(Variable::parse("field.jdk.home").is_none()); + assert!(Variable::parse("field.compiler.home").is_none()); assert!(Variable::parse("project.path").is_none()); assert!(Variable::parse("field.name|upper").is_none()); } @@ -310,8 +311,9 @@ mod tests { } #[test] - fn major_versions_follow_java_numbering() { - assert_eq!(major_version("1.8.0_412"), "8"); + fn major_versions_take_the_first_numeric_component() { + assert_eq!(major_version("8.0_412"), "8"); + assert_eq!(major_version("1.8.0"), "1"); assert_eq!(major_version("25.0.3"), "25"); assert_eq!(major_version(""), ""); } diff --git a/rust/lithe-core/src/protocol/command.rs b/rust/lithe-core/src/protocol/command.rs index b79780eb..d261453f 100644 --- a/rust/lithe-core/src/protocol/command.rs +++ b/rust/lithe-core/src/protocol/command.rs @@ -374,8 +374,6 @@ pub enum CoreCommand { ExecutionResolveEnvironment, /// Resolves deterministic process search-path ordering (`execution.resolveSearchPath`). ExecutionResolveSearchPath, - /// Moves oversized Java path-list options into a JDK argument file (`execution.planLaunchCommand`). - ExecutionPlanLaunchCommand, /// Decides how each Lithe-owned process ends (`processLifecycle.resolve`). ProcessLifecycleResolve, /// Classifies a process snapshot into lifecycle items (`processLifecycle.inventory`). @@ -999,7 +997,6 @@ impl CoreCommand { "diff.compareTexts" => Some(Self::DiffCompareTexts), "execution.resolveEnvironment" => Some(Self::ExecutionResolveEnvironment), "execution.resolveSearchPath" => Some(Self::ExecutionResolveSearchPath), - "execution.planLaunchCommand" => Some(Self::ExecutionPlanLaunchCommand), "processLifecycle.resolve" => Some(Self::ProcessLifecycleResolve), "processLifecycle.inventory" => Some(Self::ProcessLifecycleInventory), "processLifecycle.settle" => Some(Self::ProcessLifecycleSettle), diff --git a/rust/lithe-core/src/runtime/dispatcher.rs b/rust/lithe-core/src/runtime/dispatcher.rs index 857434df..25b0e142 100644 --- a/rust/lithe-core/src/runtime/dispatcher.rs +++ b/rust/lithe-core/src/runtime/dispatcher.rs @@ -1361,22 +1361,6 @@ fn execute_parsed(mut parsed: CoreRequest, document: Option<&str>) -> CoreRespon ), } } - CoreCommand::ExecutionPlanLaunchCommand => { - match serde_json::from_value::(parsed.payload) { - Ok(request) => match crate::execution::plan_launch_command(&request) { - Ok(plan) => CoreResponse::success( - id, - serde_json::to_value(plan).expect("launch plan encodes"), - ), - Err(error) => CoreResponse::failure(id, error), - }, - Err(error) => CoreResponse::failure( - id, - CoreError::new(ErrorCode::InvalidRequest, "Invalid launch command request") - .with_details(error.to_string()), - ), - } - } CoreCommand::ProcessLifecycleResolve => { match serde_json::from_value::( parsed.payload, diff --git a/rust/lithe-core/src/runtime/ffi.rs b/rust/lithe-core/src/runtime/ffi.rs index 16441f68..25e721dd 100644 --- a/rust/lithe-core/src/runtime/ffi.rs +++ b/rust/lithe-core/src/runtime/ffi.rs @@ -2,7 +2,6 @@ use std::ffi::{CStr, CString}; use std::os::raw::c_char; -use std::path::PathBuf; /// Returns a pointer to the static, NUL-terminated Core ABI version. /// @@ -95,32 +94,6 @@ pub unsafe extern "C" fn lithe_core_execute_document_json( } } -/// Loads the merged language-provider catalog for an optional workspace root. -/// -/// The returned string is owned by the caller and must be released exactly -/// once with [`lithe_core_free_string`]. -/// -/// # Safety -/// -/// `workspace_root` must be null or point to a readable, NUL-terminated byte -/// string for the duration of this call. -#[no_mangle] -pub unsafe extern "C" fn lithe_core_lsp_provider_catalog_json( - workspace_root: *const c_char, -) -> *mut c_char { - let root = if workspace_root.is_null() { - None - } else { - let value = CStr::from_ptr(workspace_root).to_string_lossy(); - if value.trim().is_empty() { - None - } else { - Some(PathBuf::from(value.as_ref())) - } - }; - response_pointer(&crate::lsp::provider_catalog_json(root.as_deref())) -} - /// Requests cooperative cancellation of an in-flight operation. The call is /// thread-safe and returns 1 when an active operation was found. /// diff --git a/rust/lithe-core/src/tests/build_project_options.rs b/rust/lithe-core/src/tests/build_project_options.rs index 35370434..55cfb88f 100644 --- a/rust/lithe-core/src/tests/build_project_options.rs +++ b/rust/lithe-core/src/tests/build_project_options.rs @@ -156,11 +156,11 @@ fn native_build_options_validate_the_shared_cases_without_dropping_custom_values .unwrap(); let result = project_options_command(json!({"document":defaults})).unwrap(); assert_eq!( - result["document"]["systems"]["maven"]["savedChoices"]["profiles"], + result["document"]["systems"]["make"]["savedChoices"]["profiles"], json!(["ci", "manual"]) ); assert_eq!( - result["document"]["systems"]["maven"]["instances"]["services/a"]["offline"], + result["document"]["systems"]["make"]["instances"]["services/a"]["offline"], true ); } diff --git a/rust/lithe-core/src/tests/plugin_run_debug.rs b/rust/lithe-core/src/tests/plugin_run_debug.rs index a56c0608..f9af00b5 100644 --- a/rust/lithe-core/src/tests/plugin_run_debug.rs +++ b/rust/lithe-core/src/tests/plugin_run_debug.rs @@ -230,6 +230,42 @@ fn temporary_files_refuse_what_the_host_could_not_write_safely() { Some(absolute.as_str()), "temporaryFiles[0].contents", ), + ( + "a file with both text and bytes", + tool_run( + json!([]), + json!({}), + json!([{ "name": "a", "contents": "", "base64": "" }]), + ), + Some(absolute.as_str()), + "temporaryFiles[0]", + ), + ( + "a file with neither text nor bytes", + tool_run(json!([]), json!({}), json!([{ "name": "a" }])), + Some(absolute.as_str()), + "temporaryFiles[0]", + ), + ( + "bytes that are not standard base64", + tool_run( + json!([]), + json!({}), + json!([{ "name": "a", "base64": "not base64!" }]), + ), + Some(absolute.as_str()), + "temporaryFiles[0].base64", + ), + ( + "bytes beyond 4 MiB", + tool_run( + json!([]), + json!({}), + json!([{ "name": "a", "base64": "A".repeat((4 * 1024 * 1024 / 3 + 1) * 4) }]), + ), + Some(absolute.as_str()), + "temporaryFiles[0].base64", + ), ( "a reference to a file the start does not declare", tool_run(json!(["${temporaryFile:missing}"]), json!({}), json!([])), @@ -263,6 +299,34 @@ fn temporary_files_refuse_what_the_host_could_not_write_safely() { } } +/// Bytes pass through unchanged, so a plugin can write a file in the encoding its format +/// requires; Core never transcodes them. +#[test] +fn temporary_file_bytes_reach_the_host_as_given() { + let workspace = Workspace::new("run-start-bytes", &[]); + let directory = temporary_root("run-start-bytes-directory") + .to_string_lossy() + .into_owned(); + // 0x95 0x5C is a double-byte character whose trail byte is a backslash. + let outcome = start_run( + &workspace, + tool_run( + json!(["@${temporaryFile:args}"]), + json!({}), + json!([{ "name": "args", "base64": "lVwK" }]), + ), + Some(&directory), + ); + + assert_eq!(outcome["status"], "ok", "{outcome}"); + let path = format!("{directory}{}args", std::path::MAIN_SEPARATOR); + assert_eq!(outcome["start"]["arguments"], json!([format!("@{path}")])); + assert_eq!( + outcome["start"]["temporaryFiles"], + json!([{ "path": path, "base64": "lVwK" }]) + ); +} + #[test] fn a_start_with_four_temporary_files_is_accepted() { let workspace = Workspace::new("run-start-four-files", &[]); diff --git a/rust/lithe-core/src/tests/run_configuration.rs b/rust/lithe-core/src/tests/run_configuration.rs index 1a2ec741..746b71ab 100644 --- a/rust/lithe-core/src/tests/run_configuration.rs +++ b/rust/lithe-core/src/tests/run_configuration.rs @@ -38,9 +38,9 @@ fn generic_preference_edits_preserve_run_metadata_without_creating_legacy_defaul ); for original in [ serde_json::json!({"version":2,"configurations":[]}), - serde_json::json!({"version":1,"configurations":[{"id":"user:legacy","type":"java.main","mainClass":"fixture.Main","workingDirectory":".","future":"preserve"}]}), + serde_json::json!({"version":1,"configurations":[{"id":"user:legacy","type":"example.main","entry":"fixture.Main","workingDirectory":".","future":"preserve"}]}), serde_json::json!({"version":2,"configurations":[{"id":"user:task","env":{"TOKEN":"fixture-value"},"future":[1,2]}], - "futureMetadata":{"keep":true},"toolchain":{"java":{"homePath":"/fixture/legacy-jdk"},"futureToolchain":{"keep":true}}}), + "futureMetadata":{"keep":true},"toolchain":{"runtime":{"homePath":"/fixture/legacy-runtime"},"futureToolchain":{"keep":true}}}), ] { let result = call( "runConfig.updateToolPreferences", @@ -101,7 +101,7 @@ fn generic_preference_editor_rejects_invalid_documents_and_references_without_wr ), ( serde_json::json!({"version":2,"configurations":[]}), - serde_json::json!({"version":1,"workspace":{"tools":{"java":{"inherit":"workspace"}}}}), + serde_json::json!({"version":1,"workspace":{"tools":{"runtime":{"inherit":"workspace"}}}}), ), ] { let result: Value = serde_json::from_str(&execute_json( @@ -145,7 +145,7 @@ fn project_tool_preferences_preserve_symbolic_choices_across_legacy_editor_saves let changed = call( "runConfig.updateOptions", serde_json::json!({"root":root,"scope":"local","configurationId":"","localDocument":document, - "toolchain":{"javaHomePath":"/fixture/changed-jdk"}}), + "toolchain":{"legacyHomePath":"/fixture/changed-home"}}), ); assert_eq!(changed["ok"], true, "{changed}"); let saved: Value = serde_json::from_str(changed["data"]["document"].as_str().unwrap()).unwrap(); @@ -241,11 +241,11 @@ fn resolve_prefers_a_host_provided_local_document() { fs::create_dir_all(root.join(".lithe/run")).unwrap(); seed_generated( &root, - r#"{"version":2,"configurations":[{"id":"current-file","name":"Current File","provider":"java.current-file","execution":"application","toolchains":{"java":"project-jdk"}}]}"#, + r#"{"version":2,"configurations":[{"id":"current-file","name":"Current File","provider":"example.current-file","execution":"application","toolchains":{"runtime":"project-runtime"}}]}"#, ); fs::write( root.join(".lithe/run/local.json"), - r#"{"version":2,"configurations":[{"id":"current-file","name":"Project Local","provider":"java.current-file","cwd":"."}]}"#, + r#"{"version":2,"configurations":[{"id":"current-file","name":"Project Local","provider":"example.current-file","cwd":"."}]}"#, ) .unwrap(); @@ -260,7 +260,7 @@ fn resolve_prefers_a_host_provided_local_document() { "configurations": [{ "id": "current-file", "name": "This PC", - "provider": "java.current-file", + "provider": "example.current-file", "cwd": "." }] } @@ -291,7 +291,7 @@ fn resolve_prefers_a_host_provided_local_document() { "configurations": [{ "id": "current-file", "name": "Legacy This PC", - "type": "java.current-file", + "type": "example.current-file", "programArguments": ["--dev"] }] } @@ -450,15 +450,15 @@ fn run_configuration_inspect_reports_malformed_and_unsupported_documents() { fn run_configuration_inspection_summarizes_changed_inputs() { let root = temporary_root("run-config-input-summary"); fs::create_dir_all(root.join("src")).unwrap(); - fs::write(root.join("src/App.java"), "class App {}").unwrap(); + fs::write(root.join("src/app.txt"), "app").unwrap(); fs::write(root.join("package.json"), r#"{"name":"demo"}"#).unwrap(); - let generated = generate_run_configuration(&root, &["src/App.java"], None); - let generated_again = generate_run_configuration(&root, &["src/App.java"], None); + let generated = generate_run_configuration(&root, &["src/app.txt"]); + let generated_again = generate_run_configuration(&root, &["src/app.txt"]); assert_eq!(generated, generated_again); write_generated_run_document(&root, &generated); fs::write(root.join("package.json"), r#"{"name":"renamed"}"#).unwrap(); - let inspected = inspect_run_configuration(&root, None); + let inspected = inspect_run_configuration(&root); assert_eq!( inspected["diagnostics"][0]["message"], "Project inputs changed: 0 added, 0 removed, 1 modified" @@ -467,15 +467,8 @@ fn run_configuration_inspection_summarizes_changed_inputs() { fs::remove_dir_all(root).unwrap(); } -fn generate_run_configuration( - root: &std::path::Path, - paths: &[&str], - java_entrypoints: Option, -) -> Value { - let mut payload = serde_json::json!({ "root": root, "paths": paths, "modulePaths": [] }); - if let Some(entrypoints) = java_entrypoints { - payload["javaEntrypoints"] = entrypoints; - } +fn generate_run_configuration(root: &std::path::Path, paths: &[&str]) -> Value { + let payload = serde_json::json!({ "root": root, "paths": paths, "modulePaths": [] }); let response: Value = serde_json::from_str(&execute_json( &serde_json::json!({ "id": "generate", "command": "runConfig.generate", "payload": payload }) .to_string(), @@ -487,14 +480,11 @@ fn generate_run_configuration( fn write_generated_run_document(root: &std::path::Path, generated: &Value) { fs::create_dir_all(root.join(".lithe/run")).unwrap(); - seed_generated(&root, serde_json::to_string(generated).unwrap()); + seed_generated(root, serde_json::to_string(generated).unwrap()); } -fn inspect_run_configuration(root: &std::path::Path, java_entrypoints: Option) -> Value { - let mut payload = serde_json::json!({ "root": root }); - if let Some(entrypoints) = java_entrypoints { - payload["javaEntrypoints"] = entrypoints; - } +fn inspect_run_configuration(root: &std::path::Path) -> Value { + let payload = serde_json::json!({ "root": root }); let response: Value = serde_json::from_str(&execute_json( &serde_json::json!({ "id": "inspect", "command": "runConfig.inspect", "payload": payload }) .to_string(), @@ -511,11 +501,11 @@ fn run_configuration_resolve_matches_toolchains_and_rejects_unsafe_paths() { fs::create_dir_all(root.join(".lithe/toolchains")).unwrap(); seed_generated( &root, - r#"{"version":1,"configurations":[{"id":"current-file","name":"Current File","type":"java.current-file","toolchains":{"java":"project-jdk"}}]}"#, + r#"{"version":1,"configurations":[{"id":"current-file","name":"Current File","type":"example.current-file","toolchains":{"runtime":"project-runtime"}}]}"#, ); seed_requirements( &root, - r#"{"version":1,"toolchains":{"project-jdk":{"type":"java","minimumVersion":"21","preferredVendor":"temurin"}}}"#, + r#"{"version":1,"toolchains":{"project-runtime":{"type":"runtime","minimumVersion":"21","preferredVendor":"temurin"}}}"#, ); let resolve = |version: &str, vendor: &str| -> Value { @@ -526,8 +516,8 @@ fn run_configuration_resolve_matches_toolchains_and_rejects_unsafe_paths() { "payload": { "root": root, "toolchainCandidates": [{ - "id": "project-jdk", - "type": "java", + "id": "project-runtime", + "type": "runtime", "version": version, "vendor": vendor }] @@ -577,7 +567,7 @@ fn legacy_v2_runtime_requirements_are_reconciled_without_regeneration() { seed_generated( &root, r#"{"version":2,"configurations":[ - {"id":"spring","name":"backend","provider":"spring-boot.maven","execution":"service","cwd":".","toolchains":{"java":"project-jdk","maven":"project-maven"}}, + {"id":"service","name":"backend","provider":"build.service","execution":"service","cwd":".","toolchains":{"runtime":"project-runtime","build":"project-build"}}, {"id":"npm","name":"web","provider":"npm.script","execution":"service","command":"npm","args":["run","dev"],"cwd":"web","toolchains":{}}, {"id":"bun","name":"bun web","provider":"npm.script","execution":"service","command":"bun","args":["run","dev"],"cwd":"bun-web","toolchains":{"runtime":"project-node"}} ]}"#, @@ -585,8 +575,8 @@ fn legacy_v2_runtime_requirements_are_reconciled_without_regeneration() { seed_requirements( &root, r#"{"version":1,"toolchains":{ - "project-jdk":{"type":"java"}, - "project-maven":{"type":"maven","java":"project-jdk"}, + "project-runtime":{"type":"runtime"}, + "project-build":{"type":"build-tool"}, "project-node":{"type":"node","minimumVersion":"22"} }}"#, ); @@ -598,8 +588,8 @@ fn legacy_v2_runtime_requirements_are_reconciled_without_regeneration() { "payload": { "root": root, "toolchainCandidates": [ - {"id":"project-jdk","type":"java","version":"21","vendor":"Temurin"}, - {"id":"project-maven","type":"maven","version":"3.9.9","vendor":""} + {"id":"project-runtime","type":"runtime","version":"21","vendor":"Example"}, + {"id":"project-build","type":"build-tool","version":"3.9.9","vendor":""} ] } }) @@ -656,7 +646,7 @@ fn shared_run_configuration_fixtures_have_the_versioned_contract_shape() { #[test] fn shared_hybrid_fixture_executes_scoped_toolchain_expectations() { let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../fixtures/run-configuration/hybrid-spring-vue.json"); + .join("../fixtures/run-configuration/hybrid-service-web.json"); let fixture: Value = serde_json::from_str(&fs::read_to_string(fixture_path).unwrap()).unwrap(); let root = temporary_root("run-config-hybrid-fixture"); fs::create_dir_all(root.join(".lithe/run")).unwrap(); @@ -674,8 +664,8 @@ fn shared_hybrid_fixture_executes_scoped_toolchain_expectations() { "payload": { "root": root, "toolchainCandidates": [ - {"id":"project-jdk","type":"java","version":"21","vendor":"Temurin"}, - {"id":"project-maven","type":"maven","version":"3.9.9","vendor":""} + {"id":"project-runtime","type":"runtime","version":"21","vendor":"Example"}, + {"id":"project-build","type":"build-tool","version":"3.9.9","vendor":""} ] } }) @@ -725,7 +715,7 @@ fn shared_editor_save_fixture_executes_the_document_contract() { fs::create_dir_all(root.join("backend")).unwrap(); seed_generated( &root, - r#"{"version":2,"configurations":[{"id":"spring","name":"Spring","provider":"spring-boot.maven","execution":"service","toolchains":{"java":"project-jdk","maven":"project-maven"},"extensions":{"maven":{"module":"."}}}]}"#, + r#"{"version":2,"configurations":[{"id":"server","name":"Server","provider":"build.service","execution":"service","toolchains":{"runtime":"project-runtime"},"extensions":{"build":{"module":"."}}}]}"#, ); let mut request = fixture["request"].clone(); @@ -753,7 +743,7 @@ fn migration_leaves_sidecar_documents_at_their_own_version() { fs::create_dir_all(root.join(".lithe/toolchains")).unwrap(); seed_generated( &root, - r#"{"version":1,"configurations":[{"id":"current-file","name":"Current File","type":"java.current-file"}]}"#, + r#"{"version":1,"configurations":[{"id":"current-file","name":"Current File","type":"example.current-file"}]}"#, ); fs::write( root.join(".lithe/project.json"), @@ -778,10 +768,10 @@ fn migration_leaves_sidecar_documents_at_their_own_version() { fs::remove_dir_all(root).unwrap(); } -/// A non-Java service must reach a launch plan without acquiring a Java -/// toolchain or a JAVA_HOME it has no use for. +/// A service that names no toolchain reaches a launch plan with its own +/// command and no toolchain binding. #[test] -fn process_configurations_launch_without_java_assumptions() { +fn process_configurations_launch_without_a_toolchain() { let root = temporary_root("run-config-process"); fs::create_dir_all(root.join(".lithe/run")).unwrap(); fs::create_dir_all(root.join("frontend")).unwrap(); @@ -816,7 +806,6 @@ fn process_configurations_launch_without_java_assumptions() { assert_eq!(plan["data"]["arguments"], serde_json::json!(["run", "dev"])); assert_eq!(plan["data"]["workingDirectory"], "frontend"); assert_eq!(plan["data"]["env"]["PORT"], "3000"); - assert!(plan["data"]["environment"]["JAVA_HOME"].is_null()); fs::remove_dir_all(root).unwrap(); } @@ -851,14 +840,13 @@ fn toolchain_backed_process_uses_the_generic_runtime_binding() { serde_json::json!(["run", "./cmd/api"]) ); assert_eq!(plan["data"]["env"]["APP_ENV"], "dev"); - assert!(plan["data"]["environment"]["JAVA_HOME"].is_null()); fs::remove_dir_all(root).unwrap(); } #[test] -fn pure_go_generation_does_not_require_java_or_add_java_current_file() { - let root = temporary_root("pure-go-no-jdk"); +fn pure_go_generation_adds_no_current_file_configuration() { + let root = temporary_root("pure-go-generation"); fs::create_dir_all(&root).unwrap(); fs::write(root.join("go.mod"), "module example.com/api\n\ngo 1.24\n").unwrap(); fs::write(root.join("main.go"), "package main\nfunc main() {}\n").unwrap(); @@ -882,7 +870,6 @@ fn pure_go_generation_does_not_require_java_or_add_java_current_file() { assert!(!configurations .iter() .any(|value| value["id"] == "current-file")); - assert!(generated["data"]["toolchainRequirements"]["toolchains"]["project-jdk"].is_null()); fs::remove_dir_all(root).unwrap(); } @@ -934,7 +921,6 @@ fn multi_language_generation_declares_only_consumed_runtime_requirements() { for id in ["project-go", "project-python", "project-cargo"] { assert!(requirements[id].is_null(), "{id}: {requirements}"); } - assert!(requirements["project-jdk"].is_null(), "{requirements}"); for provider in ["go.main", "python.script", "cargo.binary"] { let configuration = generated["data"]["generated"]["configurations"] .as_array() @@ -949,7 +935,7 @@ fn multi_language_generation_declares_only_consumed_runtime_requirements() { } #[test] -fn unknown_provider_without_an_executable_never_falls_into_maven() { +fn unknown_provider_without_an_executable_is_rejected() { let root = temporary_root("run-config-unknown-provider"); fs::create_dir_all(root.join(".lithe/run")).unwrap(); seed_generated( @@ -979,8 +965,8 @@ fn unknown_provider_without_an_executable_never_falls_into_maven() { } /// Generic editor options must patch the common process shape. Writing -/// them into extensions.maven makes the UI appear to save successfully -/// while Go/Python/Node launch plans continue using the old arguments. +/// them into a provider extension makes the UI appear to save successfully +/// while process launch plans continue using the old arguments. #[test] fn process_options_update_common_arguments_and_environment() { let root = temporary_root("run-config-process-options"); @@ -1023,7 +1009,7 @@ fn process_options_update_common_arguments_and_environment() { serde_json::json!(["app.py", "--port", "9000"]) ); assert_eq!(patch["env"]["APP_ENV"], "test"); - assert!(patch["extensions"]["maven"].is_null()); + assert!(patch.get("extensions").is_none(), "{patch}"); fs::write( root.join(".lithe/run/local.json"), @@ -1094,11 +1080,11 @@ fn resolve_chooses_detected_default_unless_the_project_names_one() { seed_generated( &root, serde_json::json!({"version": 2, "configurations": [ - {"id": "current-file", "name": "Current File", "provider": "java.current-file", + {"id": "current-file", "name": "Current File", "provider": "example.current-file", "execution": "application", "toolchains": {}}, - {"id": "java-main:demo.Tool", "name": "Tool", "provider": "java.main", + {"id": "example-main:demo.Tool", "name": "Tool", "provider": "example.main", "execution": "application", "toolchains": {}}, - {"id": "spring-boot.maven:.", "name": "app", "provider": "spring-boot.maven", + {"id": "build.service:.", "name": "app", "provider": "build.service", "execution": "service", "toolchains": {}} ]}), ); @@ -1106,19 +1092,19 @@ fn resolve_chooses_detected_default_unless_the_project_names_one() { assert_eq!(detected["ok"], true, "{detected}"); assert_eq!( detected["data"]["defaultRunConfiguration"], - "spring-boot.maven:." + "build.service:." ); fs::create_dir_all(root.join(".lithe")).unwrap(); fs::write( root.join(".lithe/project.json"), - r#"{"version":1,"defaultRunConfiguration":"java-main:demo.Tool"}"#, + r#"{"version":1,"defaultRunConfiguration":"example-main:demo.Tool"}"#, ) .unwrap(); let named = run_config_command("runConfig.resolve", serde_json::json!({ "root": root })); assert_eq!( named["data"]["defaultRunConfiguration"], - "java-main:demo.Tool" + "example-main:demo.Tool" ); } @@ -1197,24 +1183,6 @@ fn working_directory_override_must_name_an_existing_project_directory() { assert_eq!(document["configurations"][0]["cwd"], "backend"); } -#[test] -fn java_selection_rejects_ambiguous_or_invalid_host_snapshots() { - for payload in [ - serde_json::json!({"root":"/unused","requirements":{"version":1,"toolchains":{}},"candidates":[]}), - serde_json::json!({"requirements":{"version":999,"toolchains":{}},"candidates":[]}), - serde_json::json!({"requirements":{"toolchains":{}},"candidates":[]}), - ] { - let response: Value = serde_json::from_str(&execute_json( - &serde_json::json!({ - "id":"snapshot","command":"runConfig.selectJava","payload":payload - }) - .to_string(), - )) - .unwrap(); - assert_eq!(response["ok"], false, "{response}"); - } -} - #[test] fn shared_run_scan_excludes_caches_and_keeps_hidden_sources() { let root = temporary_root("shared-run-scan"); @@ -1400,7 +1368,7 @@ fn build_provider_configurations_persist_before_generation_and_reuse_identity() ("projectID", serde_json::json!("")), ("userArguments", serde_json::json!(["bad\u{0}"])), ("userArguments", serde_json::json!(vec!["a"; 65])), - ("command", serde_json::json!("java")), + ("command", serde_json::json!("runtime")), ] { let mut invalid = fixture["reference"].clone(); invalid[field] = value; @@ -1419,7 +1387,7 @@ fn build_provider_configurations_persist_before_generation_and_reuse_identity() serde_json::json!([]) ); let mut invalid = document.clone(); - invalid["configurations"][0]["command"] = serde_json::json!("java"); + invalid["configurations"][0]["command"] = serde_json::json!("runtime"); fs::write(&local, invalid.to_string()).unwrap(); assert_eq!( call("runConfig.resolve", serde_json::json!({"root":root}))["ok"], @@ -1757,7 +1725,7 @@ fn provider_run_options_reject_malformed_or_oversized_values() { } } -/// A non-Java provider owns discovery; regenerating labels must retain user choices, +/// A build provider owns discovery; regenerating labels must retain user choices, /// while withdrawing a target must not resurrect it from the previous detected layer. #[test] fn generic_provider_runs_regenerate_without_language_facts_and_keep_stable_overrides() { @@ -1796,7 +1764,14 @@ fn generic_provider_runs_regenerate_without_language_facts_and_keep_stable_overr let id = configuration["id"].clone(); assert_eq!(configuration["provider"], "build.provider"); assert!(configuration["toolchains"].as_object().unwrap().is_empty()); - assert!(configuration["extensions"].get("maven").is_none()); + assert!( + configuration["extensions"] + .as_object() + .unwrap() + .keys() + .all(|key| key == "build"), + "{configuration}" + ); fs::write( root.join(".lithe/run/local.json"), serde_json::json!({"version":2,"configurations":[ @@ -1859,7 +1834,7 @@ fn generic_provider_runs_regenerate_without_language_facts_and_keep_stable_overr assert!(!root.join(".lithe/run/generated.json").exists()); } -/// Providers receive saved typed choices; Core does not assemble a Maven goal or reread its POM. +/// Providers receive saved typed choices; Core does not assemble a build goal or reread a build file. #[test] fn provider_launch_inputs_preserve_document_layers_and_current_selector() { let root = temporary_root("provider-launch-inputs"); @@ -1873,7 +1848,11 @@ fn provider_launch_inputs_preserve_document_layers_and_current_selector() { } } let _cleanup = Cleanup(root.clone()); - fs::write(root.join("nested/pom.xml"), "deliberately not a POM").unwrap(); + fs::write( + root.join("nested/build.manifest"), + "deliberately not a manifest", + ) + .unwrap(); let generated = serde_json::json!({"version":2,"configurations":[{"id":"service","name":"Service","provider":"build.provider","execution":"service", "cwd":"nested","env":{},"extensions":{"build":{"pluginID":"dev.example.engine","buildSystemID":"engine","root":"nested","run":"serve", "projectID":"project","targetID":"target","execution":"service"},"build-run":{"runtimeArguments":["--message=base value"]}}}]}); @@ -1938,14 +1917,9 @@ fn provider_launch_inputs_require_a_typed_reference_after_domain_migration() { } } let _cleanup = Cleanup(root.clone()); - for provider in [ - "maven.module", - "spring-boot.maven", - "quarkus.maven", - "micronaut.maven", - ] { + for provider in ["legacy.module", "legacy.framework", "legacy.service"] { let generated = serde_json::json!({"version":2,"configurations":[{"id":"legacy","name":"Legacy", - "provider":provider,"execution":"task","extensions":{"maven":{"module":".","profiles":["saved"],"skipTests":false}}}]}); + "provider":provider,"execution":"task","extensions":{"legacy":{"module":".","profiles":["saved"],"skipTests":false}}}]}); seed_generated(&root, generated.to_string()); let reply: Value = serde_json::from_str(&execute_json( &serde_json::json!({"id":"unmigrated","command":"runConfig.providerLaunchInputs", diff --git a/rust/lithe-core/src/tests/toolchain_pins.rs b/rust/lithe-core/src/tests/toolchain_pins.rs index 6f1ceb8e..b66657be 100644 --- a/rust/lithe-core/src/tests/toolchain_pins.rs +++ b/rust/lithe-core/src/tests/toolchain_pins.rs @@ -56,7 +56,7 @@ fn toolchain_pins_do_not_match_sibling_prefixes_or_other_tools() { fn toolchain_pins_retain_dormant_namespaced_values_and_legacy_data() { let document = json!({ "version":1, - "toolchains":{"project-jdk":{"home":"relative legacy home","javaHome":"legacy JDK"},"custom old key":{"executable":"legacy-command"}}, + "toolchains":{"project-runtime":{"home":"relative legacy home","legacyHome":"legacy runtime"},"custom old key":{"executable":"legacy-command"}}, "pins":{".":{"dev.lithe.fixture.disabled:compiler":pin("/opt/lithe-fixtures/compiler")}} }); let before = document.clone(); @@ -192,7 +192,16 @@ fn toolchain_pins_reject_invalid_tools_providers_and_unknown_fields() { let mut document = local_pins(); document["pins"]["."]["java"]["extra"] = json!(true); assert!(pins::validate(&document).is_err()); + // Legacy entries are opaque string records: any key is kept, but a value must + // be a string and an entry holds at most 16 properties. let document = json!({"version":1,"toolchains":{"old":{"unknown":"path"}}}); + assert!(pins::validate(&document).is_ok()); + let document = json!({"version":1,"toolchains":{"old":{"unknown":true}}}); + assert!(pins::validate(&document).is_err()); + let crowded: serde_json::Map = (0..17) + .map(|index| (format!("key{index}"), json!("path"))) + .collect(); + let document = json!({"version":1,"toolchains":{"old":crowded}}); assert!(pins::validate(&document).is_err()); assert!(pins::resolve( &json!({"document":local_pins(),"directory":".","tool":"java","unknown":true}) diff --git a/rust/lithe-core/src/toolchain/pins.rs b/rust/lithe-core/src/toolchain/pins.rs index e7c7d8b9..46383d61 100644 --- a/rust/lithe-core/src/toolchain/pins.rs +++ b/rust/lithe-core/src/toolchain/pins.rs @@ -7,6 +7,7 @@ const MAX_DIRECTORIES: usize = 1024; const MAX_TOOLS_PER_DIRECTORY: usize = 64; const MAX_DIRECTORY_BYTES: usize = 2048; const MAX_PATH_BYTES: usize = 4096; +const MAX_LEGACY_PROPERTIES: usize = 16; /// Resolves a stored pin without granting execution authority or modifying the document. /// Callers must independently require an active definition and validate native files. @@ -143,10 +144,11 @@ pub(crate) fn validate(document: &Value) -> Validation<()> { for (id, value) in legacy { text(&Value::String(id.clone()), "document.toolchains", 192)?; let field = format!("document.toolchains.{id}"); - // The legacy key set mirrors toolchain-local-v1.schema.json so documents written - // by earlier releases keep validating. Legacy paths retain their original - // meaning; validation must not migrate or interpret them. - let entry = object(value, &field, &["home", "executable", "javaHome"])?; + // Legacy entries are opaque string records so documents written by earlier + // releases keep validating whichever keys a tool used. Core neither names + // nor interprets those keys; validation only bounds their count and size. + let entry = object(value, &field, &[])?; + count(entry.len(), MAX_LEGACY_PROPERTIES, &field)?; for (property, value) in entry { text(value, &format!("{field}.{property}"), MAX_PATH_BYTES)?; } diff --git a/rust/lithe-ide-host/src/catalog.rs b/rust/lithe-ide-host/src/catalog.rs index 4eef9285..8b0df5d1 100644 --- a/rust/lithe-ide-host/src/catalog.rs +++ b/rust/lithe-ide-host/src/catalog.rs @@ -98,12 +98,6 @@ fn schema(properties: Value, required: &[&str]) -> Value { pub fn tools() -> Vec { let definitions = vec![ ("lithe_project_inspect", "Inspect the connected IDE project, readiness and available permissions.", "read", schema(json!({}), &[])), - ("lithe_environment_inspect", "Discover Java and Maven installations and inspect saved and effective project environment settings. Does not install tools.", "read", schema(json!({}), &[])), - ("lithe_environment_configure", "Update selected project-local Java/Maven defaults using the IDE settings workflow. Empty paths restore automatic selection. Other settings are preserved. Requires configure permission.", "configure", schema(json!({"javaHomePath":string(),"mavenExecutablePath":string(),"mavenJavaHomePath":string(),"settingsPath":string(),"localRepositoryPath":string()}), &[])), - ("lithe_maven_inspect", "Read Maven modules, available/selected profiles, skip-tests setting and reload state.", "read", schema(json!({}), &[])), - ("lithe_maven_configure", "Save Maven profiles and skip-tests settings through the existing IDE workflow. Requires configure permission.", "configure", schema(json!({"profiles":{"type":"array","items":string(),"maxItems":64},"skipTests":{"type":"boolean"}}), &[])), - ("lithe_maven_reload", "Reload the Maven project and synchronize the existing Java language service. May resolve dependencies and execute project tooling. Requires execute permission.", "execute", schema(json!({}), &[])), - ("lithe_maven_execute", "Windows Maven execution (macOS uses lithe_build_execute). Execute Maven goals through the IDE, using saved profiles and toolchains. Returns operationID for output/status/stop. May execute project code and access the network. Requires execute permission.", "execute", schema(json!({"goals":{"type":"array","items":string(),"minItems":1,"maxItems":32},"modulePath":string()}), &["goals"])), ("lithe_build_inspect", "List detected build plugin instances, current project/target/model task identities and declared tasks. No tooling is executed.", "read", schema(json!({}), &[])), ("lithe_build_reload", "Ask a detected build plugin to load or reload its project model. May execute project tooling and access the network. Inspect the model state before selecting projects. Requires execute permission.", "execute", schema(json!({"instanceID":string()}), &["instanceID"])), ("lithe_build_configure", "Set one declared build option using the same instance editor and persistent store as the IDE. Read option IDs, kinds and effective values from build inspect. Null resets that option. Invalid values are refused. Native files use the explicit native configuration save workflow. Requires configure permission.", "configure", schema(json!({"instanceID":string(),"optionID":string(),"value":option_value()}), &["instanceID","optionID","value"])), @@ -187,39 +181,6 @@ fn validate_catalog( return Err(invalid()); } } - if let Some(profiles) = args.get("profiles").and_then(Value::as_array) { - if profiles.iter().any(|p| { - p.as_str() - .is_none_or(|s| s.trim().is_empty() || s.trim() != s || s.contains(',')) - }) { - return Err(invalid()); - } - } - if let Some(module) = args.get("modulePath").and_then(Value::as_str) { - if module.starts_with('/') - || module.contains(['\\', ':']) - || module.split('/').any(|s| s == "..") - { - return Err(invalid()); - } - } - // A goal is one Maven token. Shell strings, flags and implicit filesystem overrides - // belong to the IDE's settings/configuration workflow, not this execution surface. - if let Some(goals) = args.get("goals").and_then(Value::as_array) { - if goals.iter().any(|g| { - g.as_str().is_none_or(|s| { - s.starts_with('-') - || !s - .chars() - .all(|c| c.is_ascii_alphanumeric() || ":._-".contains(c)) - }) - }) { - return Err(failure( - "INVALID_ARGUMENTS", - "Use Maven goal names; configure profiles and paths through the settings tools", - )); - } - } Ok(()) } @@ -345,8 +306,8 @@ mod tests { ); assert_eq!( validate_exposed( - "lithe_maven_configure", - &json!({"profiles":["ci"]}), + "lithe_unknown_capability", + &json!({}), &permissions, &exposure ) @@ -355,7 +316,7 @@ mod tests { ); assert!(validate_exposed( "lithe_build_execute", - &json!({"instanceID":"maven:.","taskID":"compile"}), + &json!({"instanceID":"example:.","taskID":"compile"}), &Permissions::default(), &exposure ) @@ -404,37 +365,10 @@ mod tests { } } #[test] - fn authorization_and_argument_validation_cannot_be_bypassed() { - assert!(validate( - "lithe_maven_execute", - &json!({"goals":["verify"]}), - &Permissions::default() - ) - .is_err()); - let permissions = Permissions { - execute: true, - configure: true, - }; - for args in [ - json!({"goals":["verify"],"modulePath":"../other"}), - json!({"goals":["verify;curl"]}), - json!({"goals":["-f"]}), - json!({"goals":["verify"],"confirmed":true}), - ] { - assert!(validate("lithe_maven_execute", &args, &permissions).is_err()); - } - assert!(validate( - "lithe_maven_execute", - &json!({"goals":["clean","verify"],"modulePath":"module"}), - &permissions - ) - .is_ok()); - } - #[test] fn build_authorization_and_argument_validation_cannot_be_bypassed() { assert!(validate( "lithe_build_execute", - &json!({"instanceID":"maven@.","taskID":"verify"}), + &json!({"instanceID":"fixture@.","taskID":"verify"}), &Permissions::default() ) .is_err()); @@ -443,14 +377,14 @@ mod tests { configure: true, }; for args in [ - json!({"instanceID":"maven@."}), - json!({"instanceID":"maven@.","taskID":"verify","command":"mvn"}), - json!({"instanceID":"maven@.","taskID":"verify","userArguments":["bad\u{0000}argument"]}), - json!({"instanceID":"maven@.","taskID":"verify","confirmed":true}), + json!({"instanceID":"fixture@."}), + json!({"instanceID":"fixture@.","taskID":"verify","command":"fixture-tool"}), + json!({"instanceID":"fixture@.","taskID":"verify","userArguments":["bad\u{0000}argument"]}), + json!({"instanceID":"fixture@.","taskID":"verify","confirmed":true}), ] { assert!(validate("lithe_build_execute", &args, &permissions).is_err()); } - assert!(validate("lithe_build_execute", &json!({"instanceID":"maven@.","taskID":"custom","projectID":"api","userArguments":["clean","verify"]}), &permissions).is_ok()); + assert!(validate("lithe_build_execute", &json!({"instanceID":"fixture@.","taskID":"custom","projectID":"api","userArguments":["clean","verify"]}), &permissions).is_ok()); } #[test] fn build_options_require_configure_and_accept_only_bounded_declared_value_shapes() { diff --git a/rust/lithe-ide-host/src/lib.rs b/rust/lithe-ide-host/src/lib.rs index 8582ae13..f810cb8a 100644 --- a/rust/lithe-ide-host/src/lib.rs +++ b/rust/lithe-ide-host/src/lib.rs @@ -591,7 +591,7 @@ mod tests { let denied: Value = client .post(&connection.endpoint) .bearer_auth(&connection.token) - .json(&json!({"name":"lithe_build_execute","arguments":{"instanceID":"maven@.","taskID":"verify"}})) + .json(&json!({"name":"lithe_build_execute","arguments":{"instanceID":"fixture@.","taskID":"verify"}})) .send() .await .unwrap() @@ -691,13 +691,14 @@ mod tests { "../../fixtures/ide-api/host-tool-exposure.json" )) .unwrap(); - let (_fixture, broker, headers) = push_fixture_exposed(exposure["macos"].clone()); + let mut advertised = exposure["macos"].clone(); + advertised + .as_array_mut() + .unwrap() + .retain(|tool| tool["name"] != "lithe_run_list"); + let (_fixture, broker, headers) = push_fixture_exposed(advertised); for (name, arguments, code) in [ - ( - "lithe_maven_execute", - json!({"goals":["verify"]}), - "NOT_SUPPORTED", - ), + ("lithe_run_list", json!({}), "NOT_SUPPORTED"), ( "lithe_build_configure", json!({"instanceID":"example:.","optionID":"offline","value":true,"settingsPath":"/fixture/settings.xml"}), @@ -757,13 +758,7 @@ mod tests { configure: true, execute: true, }; - assert!(catalog::validate_served( - "lithe_maven_execute", - &json!({"goals":["verify"]}), - &permissions, - None - ) - .is_ok()); + assert!(catalog::validate_served("lithe_run_list", &json!({}), &permissions, None).is_ok()); } async fn notification(rx: &mut tokio::sync::mpsc::UnboundedReceiver) -> Option { diff --git a/shared/plugins/contract/v1/README.md b/shared/plugins/contract/v1/README.md index 177f83f4..a1af66c2 100644 --- a/shared/plugins/contract/v1/README.md +++ b/shared/plugins/contract/v1/README.md @@ -77,7 +77,7 @@ and resolution. A separate fresh resolution serves Build tools and run plans whose `toolchain` executable names a plugin-defined tool rather than a host toolchain; settings detections never grant launch authority. Saving pins, `toolchainConsumers`, generic runtime launches and live backend activation remain -unsupported, and Java and language-server consumers keep their previous adapters. Version-manager +unsupported, and language-server consumers keep their previous adapters. Version-manager implementations such as mise belong to their own plugin packages. See [the implemented slice](toolchains/README.md). @@ -294,7 +294,7 @@ activation event, and a plugin may consist of grammars alone. | `validationArguments` | Probe that must exit with status 0 before a candidate is used; empty means the host trusts the candidate and lets the LSP handshake decide. | | `runtimeDependencies` | Tools the host must discover and pass to the server, each a tool-definition ID that `requiresTools` lists (see [Tool references](#tool-references)). The host exports each path as `LITHE__PATH` (`LITHE_NODE_PATH`, `LITHE_GO_PATH`; a user-set value takes precedence) and prepends its directory to the server's `PATH`, because servers such as gopls run the tool themselves and an app opened from the Dock does not inherit the shell `PATH`. A project-local copy is never chosen, since servers start automatically when a file opens. | | `initializationOptions` | A JSON object sent as the LSP `initializationOptions`. Every string value inside it is a launch template. | -| `hostIntegration` | Any plugin may declare one. A named integration implemented in the host, with the package resources it reads; see [Language server host integrations](#language-server-host-integrations). | +| `hostIntegration` | Reserved. Core implements no host integration, so any ID is refused; see [Language server host integrations](#language-server-host-integrations). | | `requestMethods`, `commands`, `serializedCommands`, `forwardNotifications`, `state`, `remote` | What the plugin backend may ask of the running server and how the host keeps its state; see [Plugin-owned sessions](#plugin-owned-sessions). | The host owns the language-server process lifecycle, the LSP session, and @@ -306,59 +306,19 @@ the same meaning and validation; see [Build providers](#build-providers). #### Language server host integrations -Most of a language server is described by its launch. A few servers also need -behavior that data cannot express: a project model computed from build files, -virtual documents, test and debug wiring. That behavior is code in the host, -called a host integration. A server opts into one by name and tells it where -the files it needs are inside the plugin's own package: - -```json -"hostIntegration": { - "id": "jdt", - "capabilities": ["debug", "run", "testing"], - "resources": { "configuration": "language-server/configuration/${arch}", "lombokAgent": "…" } -} -``` - -The plugin owns the files; the host owns the behavior. The host enables an -integration only for the server that declares it and never matches on a -language ID or looks for the files anywhere else, so removing the plugin -removes the integration's inputs with it. Any plugin may declare one, whatever -its namespace. The integration is host code, but it reads only resources inside -the declaring plugin's own package and hands them to the language server or -helper process that same plugin starts (extension bundles, agents, a test -runner, debugging); it grants the plugin no permission beyond running its own -server process, which an installed plugin may already do once the user confirms -the install review. An ID the host does not implement is refused with -`plugin.integration.unsupported`. A build system's `hostIntegration` is a -different declaration and stays limited to official plugins; see +`hostIntegration` is reserved. Core implements no host integration and never +matches on a language ID, a server ID or a tool name: everything a language +needs beyond its launch comes from the plugin's own backend, providers and +declarations. Any `hostIntegration` ID is refused with +`plugin.integration.unsupported`, at `languageServers[].hostIntegration.id`, +so a manifest naming one fails validation instead of loading without it. A build +system's `hostIntegration` is a separate declaration with the same rule; see [Host integrations](#host-integrations) under [Build systems](#build-systems). -Each resource is a package-relative path, or a list of them, that may vary -only by `${arch}`. An integration defines which names it reads; a missing -required name, an unknown name, or the wrong shape is `plugin.manifest.invalid`. - A plugin that declares a language owns its process-backed features: the host stops running its built-in tools for that language, so a missing or disabled -plugin never falls back to them. `capabilities` is how the plugin hands a -feature back. It lists the features of the server's language that the host's -own implementation provides through the integration: `run`, `testing`, and -`debug`. A feature that is neither listed here nor supplied by one of the -plugin's `executionProviders` is unavailable for the language. Omitting the -list claims nothing. - -Each integration defines the capabilities it provides and the resources each -one reads. A capability the integration does not provide, a capability whose -resource is not declared, and `run` or `testing` listed for a language whose -execution provider already supplies it are all `plugin.manifest.invalid`. The -normalized manifest lists capabilities sorted and without duplicates. - -| Integration | Requires | Capabilities | Resources | -| --- | --- | --- | --- | -| `jdt` | A `runtime` launch with the `java` tool | `run`. `debug`: needs `extensionBundleDirectories`, which must contain the Java Debug Server bundle. `testing`: needs `extensionBundleDirectories` with the Java Test bundles, and `testRunner`. | `configuration` (required): the Equinox configuration directory, of which the host gives each launch a writable copy. `lombokAgent` (required): the Lombok agent JAR. `extensionBundleDirectories` (list): directories whose JAR files are loaded as JDT LS extension bundles, in declared order and then by file name. `testRunner`: the standalone Java test runner JAR. | - -The launch plan returns `hostIntegration` as `{id, resources}` with every -path expanded, and lists the paths in `requiredPackagePaths`. +plugin never falls back to them. A feature that none of the plugin's +`executionProviders` supplies is unavailable for the language. #### Workspace configuration @@ -1111,9 +1071,9 @@ back to Provider discovery. A host-managed `node` or `go` dependency may select one such candidate, while retaining Core's version probe. Any other dependency that declares no `executables` is already this candidate for the definition with its own ID, and -that definition checks `minimumVersion`. Maven declares its namespace-owned tool -and uses the short `java` definition for its Java prerequisite; no Maven-specific -JDK registry entry is added. Its backend bootstrap remains a separate consumer. +that definition checks `minimumVersion`. A tool that needs another tool as a +prerequisite names that tool's definition; Core adds no registry entry for any +particular tool. Passive import performs no version or runtime probes; a definition that requires verification is not reported as an already verified import executable. A toolchain that resolves to a project file remains project code, @@ -1411,8 +1371,8 @@ host change to add a build tool, and no host code branches on a build-system ID. One complete fixture covers each plugin-described model source: `build-static-file` reads a project file, `build-command-stdout` decodes a command's standard output, and `build-command-output-file` decodes a file the -command writes; the `hostIntegration` source, whose model a host integration -supplies, has no fixture. They use Cargo, Go, and Gradle as realistic inputs; +command writes; the reserved `hostIntegration` source has no fixture, because +Core implements no integration. They use Cargo, Go, and Gradle as realistic inputs; they are not those plugins. `build-provider` and `build-provider-language-server` show the two forms of a build provider, and `fixtures/build-systems/build-plans.json` is the golden list of plans and decoded outputs every host gets from them. @@ -1637,7 +1597,7 @@ Workspace-wide reports still require a current project model when the build system has one. Configuration validity is retained through diagnostic and report decoding; late results cannot update a changed workspace/configuration. -Java/Maven migration remains separate. No new resource/cache location is introduced. +No new resource/cache location is introduced. Canonical examples: `fixtures/build-providers/build-provider-tests.plugin.json` and `fixtures/build-providers/build-test-protocol.json`. @@ -1830,7 +1790,7 @@ Saved Run configurations keep their existing selectors and prepare again for Deb Fixtures: [native declaration](fixtures/build-providers/build-provider-debug-preparation.plugin.json) and [native result](fixtures/build-providers/build-debug-preparation.json). This does not provide -Java/JDT preparation, long-running services or cross-plugin collaboration. +preparation through a language server, long-running services or cross-plugin collaboration. The native Build tree re-resolves model IDs before preparation. Source/model, @@ -1991,8 +1951,8 @@ applications retain their existing native ownership. This facility never installs or activates a producer, invokes its private functions, grants permissions, leases a process or resolves a package version. There is no general plugin-dependency/export API. Backend authors must handle -unavailable and non-ready slots explicitly. Language integration and existing -Java/Maven business migration remain separate work. Shared fixtures: +unavailable and non-ready slots explicitly. Language integration remains +separate work. Shared fixtures: `fixtures/build-systems/build-model-producer.plugin.json` and `fixtures/build-systems/build-model-consumer.plugin.json`. @@ -2075,7 +2035,7 @@ Shared fixture: The build backend owns filtering, custom resource directories, profiles and reactor semantics. Hosts await successful completion, retain one operation identity through cancellation/configuration changes, and only then resolve and -launch Java. Per-run option/tool overrides are ephemeral host inputs to the +launch the program. Per-run option/tool overrides are ephemeral host inputs to the existing validated plan; they never mutate saved build defaults or claim that the persistent model reflects a different configuration. @@ -2170,12 +2130,12 @@ A host that declares `buildProviders` keeps providers apart: ### Host integrations -`hostIntegration` names host code that owns one concern while a built-in tool -migrates onto this model. Only official plugins may name one, and only -`maven` exists; a model, test-result, or dependency source of -`hostIntegration` needs it. Anything else is -`plugin.integration.unsupported`. New build systems use the declarative -contract or a canonical format instead. +`hostIntegration` is reserved. Core implements no build-system host +integration: every build tool is described by its plugin's data and backend, +and no host code branches on a build-system ID. Any `hostIntegration` name is +`plugin.integration.unsupported`, and a model, test-result, or dependency source +of `hostIntegration` is refused with it. Build systems use the declarative +contract, a canonical format, or a `provider` instead. ### Commands @@ -2553,8 +2513,8 @@ field, and `optional` only on a `text` or `tool` field. `maximumVersion` is a template over earlier fields naming the highest version the value may be, compared numerically by dotted components. It reads -`${field..major}` of a `tool` field: a target Java release that must not -exceed the selected JDK declares `"maximumVersion": "${field.jdk.major}"`. +`${field..major}` of a `tool` field: a target language level that must not +exceed the selected compiler declares `"maximumVersion": "${field.compiler.major}"`. While the bound expands to an empty string (no tool chosen yet) nothing is checked. @@ -2614,8 +2574,10 @@ directory). A condition on a hidden field is false. Strings in the manifest use `${…}`: `${project.name}`, `${field.}` (a text or option value, `true`/`false`, a comma-separated list, or the home directory of a tool), `${field..version}` and `${field..major}` (tool -fields only: the version of the chosen installation, and its major number, -`21` for `21.0.4` and `8` for `1.8.0_412`), and `${field..executable}` +fields only: the version of the chosen installation as its definition's +`version.pattern` captured it, and that version's first numeric component, +`21` for `21.0.4`; a definition whose tool uses another numbering, such as a +legacy `1.` prefix, normalizes it in the pattern), and `${field..executable}` (tool fields only: the path of the tool's executable in the chosen installation). The three suffixed variables are rejected when the manifest is validated if the field is not a `tool` field. Modifiers follow `|` and apply @@ -3322,9 +3284,14 @@ context it started in has closed. inside it) and the environment have the bounds of [run preparation](#provider-run-preparation). An unknown `process` field is refused. -- `temporaryFiles` is at most four `{name, contents}` files of up to 4 MiB of - text each. The host writes them into its own temporary directory before the - process starts, removes them when the run ends, and replaces +- `temporaryFiles` is at most four files of up to 4 MiB each. A file is + `{name, contents}`, text the host writes as UTF-8, or `{name, base64}`, + bytes in standard base64 the host writes unchanged; a file with both or + neither is refused at `temporaryFiles[]`. Bytes are for a format whose + encoding the plugin must choose, such as a launcher that reads its argument + file in the system code page: the plugin encodes the file and the host never + transcodes it. The host writes the files into its own temporary directory + before the process starts, removes them when the run ends, and replaces `${temporaryFile:}` in `process.arguments` and `process.environment` with the file's path. A name is unique, 1 to 64 letters, digits, `.`, `_` or `-`, and starts with a letter or digit. A plugin therefore never needs to @@ -4020,7 +3987,7 @@ command payload is a Core `invalid_request` failure instead. | `plugin.test.invalidIdentifier` | Core | Empty or multi-line test identifier | | `plugin.test.unsupportedProject` | Core | No project marker in the workspace | | `plugin.execution.unavailable` | Core | The plugin has no run/test provider for the language | -| `plugin.integration.unsupported` | Core | A host integration the host does not implement, or a build system's host integration requested by a plugin outside the official namespace | +| `plugin.integration.unsupported` | Core | A `hostIntegration` ID; Core implements none | | `plugin.launch.unavailable` | Core | The plugin has no language server for the language, no provider process for the build system, or no such VCS provider | | `plugin.build.unavailable` | Core | The plugin declares no such build system, or the build system does not declare the requested operation | | `plugin.build.invalidSelection` | Core | A build request selects a scope, target, filter, or argument the declaration does not allow, or misses a required path | diff --git a/shared/plugins/contract/v1/plugin-manifest.schema.json b/shared/plugins/contract/v1/plugin-manifest.schema.json index 50309a81..faba06b6 100644 --- a/shared/plugins/contract/v1/plugin-manifest.schema.json +++ b/shared/plugins/contract/v1/plugin-manifest.schema.json @@ -1454,14 +1454,14 @@ "enum": ["supported", "unsupported"] }, "hostIntegration": { - "description": "A named host integration and the package resources it reads.", + "description": "Reserved. Core implements no host integration, so any ID is refused with plugin.integration.unsupported.", "type": "object", "additionalProperties": false, "required": ["id"], "properties": { - "id": { "enum": ["jdt"] }, + "id": { "type": "string", "minLength": 1 }, "capabilities": { - "description": "Process-backed features of the language that the host's own implementation provides through the integration.", + "description": "Process-backed features of the language a host integration would provide.", "type": "array", "uniqueItems": true, "items": { "enum": ["debug", "run", "testing"] } @@ -2673,7 +2673,7 @@ "$ref": "#/$defs/buildVersionProbe" } }, - "description": "java, node, and go use host version probes. They may select one toolchain candidate with executables, or take only id and minimumVersion. Other runtimes declare their executables and version probe." + "description": "node and go use host version probes. They may select one toolchain candidate with executables, or take only id and minimumVersion. Other runtimes declare their executables and version probe." } }, "version": { @@ -2685,10 +2685,9 @@ } }, "hostIntegration": { - "description": "Official plugins only; the host must provide the named integration.", - "enum": [ - "maven" - ] + "description": "Reserved. Core implements no build-system host integration, so any name is refused with plugin.integration.unsupported.", + "type": "string", + "minLength": 1 }, "provider": { "$ref": "#/$defs/buildProvider" diff --git a/shared/plugins/contract/v1/toolchains/README.md b/shared/plugins/contract/v1/toolchains/README.md index f8adac31..0540dd7a 100644 --- a/shared/plugins/contract/v1/toolchains/README.md +++ b/shared/plugins/contract/v1/toolchains/README.md @@ -10,8 +10,8 @@ runs the definition's version probe on executables Core returns from `candidates`, and shows the result. Any change to a watched input reloads the whole snapshot. Nothing is saved by these pages. A separate fresh-resolution adapter serves macOS Build and Provider Run; settings detections are never used -as launch permits. Java and language-server consumers resolve their tools with -their own adapters. Windows has no +as launch permits. Language-server consumers resolve their tools with their +own adapters. Windows has no toolchain adapter and does not advertise `toolDefinitions` or `toolchainProviders`, so Core refuses packages that require them there. Consumer migration, the native query lifecycle and cache, pin saving and diff --git a/shared/plugins/guides/build-tool-integration.md b/shared/plugins/guides/build-tool-integration.md index dce41d5c..40dd078a 100644 --- a/shared/plugins/guides/build-tool-integration.md +++ b/shared/plugins/guides/build-tool-integration.md @@ -27,7 +27,6 @@ LSP 服务器负责语言分析,构建工具负责项目导入、编译、依 | 补全、跳转、诊断 | `languageServers` | 按 LSP 提供能力,不自动声明编译和测试 | | 运行文件或固定项目命令 | `executionProviders[].run` | 命令与 argv 模板,工作目录为工作区根;不提供通用目标选择或构建模型。桌面端不声明 `executionProviders` | | 运行整个工程、文件所在范围或指定测试 | `executionProviders[].test` | 测试项目标记、文件发现和命令规划;不是完整语义测试发现与结构化结果协议。桌面端不声明 `executionProviders` | -| Java 项目的运行/测试/调试 | 任何插件都可以声明 `languageServers[].hostIntegration`,例如 `jdt` | 使用宿主实现的集成;集成只读插件自己包里的资源,交给同一插件启动的语言服务器,不额外授予权限;宿主未实现的集成 ID 返回 `plugin.integration.unsupported`,见[契约](../contract/v1/README.md#language-server-host-integrations)。桌面端的声明说明没有列出语言服务器宿主集成,使用前先在桌面端验证 | | 通用 Build 工具窗口、工程模型、任务、依赖与同步 | `buildSystems`;模型需要程序计算时再加 `buildProviders` | Core 校验并规划,桌面端声明并呈现这两个能力,呈现范围见开头的宿主支持 | 当需求只是 `lua <当前文件>`、`go test ./...` 这类固定命令,契约提供执行提供者(`executionProviders`)。 @@ -187,17 +186,12 @@ Core 的计划接口本身不启动进程、不监视文件、不持久化选择 只说明声明和规划正确,仍要在真实宿主里验收。 不要把内部 dispatcher 命令写成社区插件可以调用的宿主脚本 API。 -`languageServers[].hostIntegration`(语言服务器侧)与构建系统 `model.source: "hostIntegration"` -是不同接口,开放范围也不同: - -- 语言服务器侧对所有插件开放。任何插件都可以声明宿主已实现的集成 ID(契约里的例子是 `jdt`); - 集成只读取声明插件自己包里的资源,交给同一插件启动的服务器或辅助进程,不额外授予权限。 - 宿主未实现的 ID 返回 `plugin.integration.unsupported`,见 - [契约](../contract/v1/README.md#language-server-host-integrations)。 -- 构建系统侧只限官方插件,目前只有 `maven`;官方命名空间以外的插件请求它同样得到 - `plugin.integration.unsupported`,见[契约](../contract/v1/README.md#host-integrations)。 - 不要复制官方 ID 或修改 `vendor` 来绕过这个限制。 - 社区构建插件用 `static`、`command` 或 `provider` 模型。 +`languageServers[].hostIntegration` 与构建系统 `model.source: "hostIntegration"` 都是保留字段。 +Core 不实现任何宿主集成,也不按语言、服务器或构建工具的 ID 走特殊逻辑,所以任何 ID 都返回 +`plugin.integration.unsupported`,见[语言服务器侧](../contract/v1/README.md#language-server-host-integrations) +与[构建系统侧](../contract/v1/README.md#host-integrations)的契约。语言的运行、测试与调试由插件自己的 +`executionProviders`、`buildSystems`、`buildProviders` 和后端提供;构建插件用 `static`、`command` 或 +`provider` 模型。 两侧都不是作者自己命名就能新增的机制:集成 ID 必须是宿主已经实现的。 @@ -254,7 +248,6 @@ Before publishing, still confirm that the target host declares every capability | Completion, navigation, diagnostics | `languageServers` | Features come from LSP; compile and test are not declared automatically | | Run a file or a fixed project command | `executionProviders[].run` | A command and argv template; the working directory is the workspace root; no general target selection or build model. The desktop does not declare `executionProviders` | | Run the whole project, a file's scope, or a named test | `executionProviders[].test` | Test-project markers, file discovery, and command planning; not a full semantic test-discovery or structured-results protocol. The desktop does not declare `executionProviders` | -| Run, test, and debug Java projects | Any plugin may declare `languageServers[].hostIntegration`, for example `jdt` | Uses an integration the host implements; the integration reads only resources inside the plugin's own package and hands them to the language server that same plugin starts, and grants no extra permission; an integration ID the host does not implement returns `plugin.integration.unsupported`; see the [contract](../contract/v1/README.md#language-server-host-integrations). The desktop's declarations do not list any language-server host integration, so verify on the desktop before relying on one | | A general Build tool window, project model, tasks, dependencies, and sync | `buildSystems`, plus `buildProviders` when a program must compute the model | Core validates and plans; the desktop declares and presents both capabilities, to the extent described in the host support note at the top | When all you need is a fixed command such as `lua ` or `go test ./...`, the contract offers @@ -425,19 +418,13 @@ A fixture that produces the right command only shows that the declaration and pl you still have to accept it in a real host. Do not present internal dispatcher commands as a host scripting API that community plugins can call. -`languageServers[].hostIntegration` (the language-server side) and the build-system -`model.source: "hostIntegration"` are different interfaces, and they are open to different plugins: - -- The language-server side is open to every plugin. Any plugin may declare an integration ID the host - already implements (the contract's example is `jdt`); the integration reads only - resources inside the declaring plugin's own package and hands them to the server or helper process - that same plugin starts, and grants no extra permission. An ID the host does not implement returns - `plugin.integration.unsupported`; see the [contract](../contract/v1/README.md#language-server-host-integrations). -- The build-system side is limited to official plugins, and currently has only `maven`; a plugin - outside the official namespace that asks for it also gets `plugin.integration.unsupported`; - see the [contract](../contract/v1/README.md#host-integrations). - Do not copy an official ID or change `vendor` to get around this limit. - Community build plugins use the `static`, `command`, or `provider` models. +`languageServers[].hostIntegration` and the build-system `model.source: "hostIntegration"` are both +reserved. Core implements no host integration and runs no special logic for a language, server, or +build-tool ID, so every ID returns `plugin.integration.unsupported`; see the contract for the +[language-server side](../contract/v1/README.md#language-server-host-integrations) and the +[build-system side](../contract/v1/README.md#host-integrations). A language's run, test, and debug +come from the plugin's own `executionProviders`, `buildSystems`, `buildProviders`, and backend; build +plugins use the `static`, `command`, or `provider` models. Neither side lets an author add an integration by picking a name: the ID must be one the host already implements. diff --git a/shared/plugins/reference/api-catalog.md b/shared/plugins/reference/api-catalog.md index 940bb246..efd35a78 100644 --- a/shared/plugins/reference/api-catalog.md +++ b/shared/plugins/reference/api-catalog.md @@ -165,7 +165,7 @@ Lithe 的插件公共 API 按 [API 实施计划](https://github.com/1lck/Lithe-I | 编辑器 Provider 单次调用时限 | hover、completion 400 ms,definition 1500 ms,其余类型 2000 ms;迟到的答复直接丢弃,注册不能自定义时限 | | `languageServer/request` | `timeoutMilliseconds` 1–300000 ms,默认 60000;每个会话最多 128 个等待 gate 打开的请求,第 129 个返回 `plugin.host.busy` | | `languageServer/setStatus` | `message` 按字符边界截到 1024 字节 | -| `run/start` | `label` 1–256 字符;`temporaryFiles` 最多 4 个,每个文本 ≤4 MiB;`rerun.data` ≤64 KiB | +| `run/start` | `label` 1–256 字符;`temporaryFiles` 最多 4 个,每个文本或 base64 字节 ≤4 MiB;`rerun.data` ≤64 KiB | | `debug/start` | `configuration` ≤256 KiB | | 内容提供者 | 提供的内容 ≤1 MiB,宿主等待 30 秒 | | `functions/invoke` | `timeoutMilliseconds` 默认 60000 ms,上限 600000 ms;`parentOperationID` 1–128 字节 | @@ -337,7 +337,7 @@ Notifications the host sends to a backend (the backend answers none), also prefi | Editor provider call budget | 400 ms for hover and completion, 1500 ms for definition, 2000 ms for the other types; a late answer is dropped, and a registration cannot set its own budget | | `languageServer/request` | `timeoutMilliseconds` 1–300000 ms, default 60000; at most 128 requests per session waiting for a gate to open, and the 129th returns `plugin.host.busy` | | `languageServer/setStatus` | `message` is cut to 1024 bytes on a character boundary | -| `run/start` | `label` 1–256 characters; `temporaryFiles` at most 4, each up to 4 MiB of text; `rerun.data` up to 64 KiB | +| `run/start` | `label` 1–256 characters; `temporaryFiles` at most 4, each up to 4 MiB of text or base64 bytes; `rerun.data` up to 64 KiB | | `debug/start` | `configuration` up to 256 KiB | | Content providers | Provided content up to 1 MiB; the host waits 30 seconds | | `functions/invoke` | `timeoutMilliseconds` defaults to 60000 ms, capped at 600000 ms; `parentOperationID` is 1–128 bytes | diff --git a/shared/plugins/reference/domains/backend.md b/shared/plugins/reference/domains/backend.md index 501c8f39..94bf3bfc 100644 --- a/shared/plugins/reference/domains/backend.md +++ b/shared/plugins/reference/domains/backend.md @@ -148,7 +148,7 @@ Core 把中转的调用以 `lithe/function/invoke` 转给被调方。和任何 - `label`:1–256 个字符,不含控制字符,作为 Run 窗口条目的标题。 - `process`:`{executable, arguments, workingDirectory, environment}`,四个字段都必填,未知字段被拒绝。`executable` 是 `{kind: "tool", tool}`(`tool` 是清单 `requiresTools` 列出的工具定义 ID,宿主按[工具链](toolchains.md)的分层解析)或 `{kind: "packaged", path}`(插件包内文件的规范化相对路径)。参数、工作目录(相对工作区根、且是其中已存在的目录)和环境的上限同[运行准备](../../contract/v1/README.md#provider-run-preparation)。 -- `temporaryFiles`:最多 4 个 `{name, contents}`,每个文本最多 4 MiB。宿主在自己的临时目录写入,运行结束时删除,并把 `process.arguments` 和 `process.environment` 里的 `${temporaryFile:}` 替换成文件路径,插件不需要知道临时目录在哪。`name` 唯一,1–64 个字母、数字、`.`、`_`、`-`,以字母或数字开头。 +- `temporaryFiles`:最多 4 个文件,每个最多 4 MiB。`{name, contents}` 是文本,宿主按 UTF-8 写入;`{name, base64}` 是标准 base64 字节,宿主原样写入,不转码,用于编码必须由插件决定的格式(例如按系统代码页读取参数文件的启动器)。两者都有或都没有时在 `temporaryFiles[]` 被拒绝。宿主在自己的临时目录写入,运行结束时删除,并把 `process.arguments` 和 `process.environment` 里的 `${temporaryFile:}` 替换成文件路径,插件不需要知道临时目录在哪。`name` 唯一,1–64 个字母、数字、`.`、`_`、`-`,以字母或数字开头。 - `debug`:`{configurationType, target}`,让这次运行成为调试会话,见[调试适配器](debug-adapters.md#由插件后端发起调试会话)。`configurationType` 是公开的调试适配器别名;`target` 是 `{kind: "connection", port}`(进程在 `port`,1–65535,监听调试器,由后端写进参数,宿主等待并附加适配器)或 `{kind: "launch"}`(适配器自己启动进程,按 `targetBindings` 从 `process` 取程序、参数、目录和环境)。 - `rerun`:`{function, data}`,插件已注册的函数和它的参数(最多 64 KiB 的 JSON 对象)。用户选“再次运行”时,宿主以 `data` 为参数、当前 `context` 调用该函数,后端再回答一次新的 `run/start`,所以重启由后端准备;没有 `rerun` 的条目不能再次运行。 @@ -368,7 +368,7 @@ Notification (the backend does not reply): `lithe/host/run/didExit` with `{runID - `label`: 1 to 256 characters without control characters; it titles the Run window entry. - `process`: `{executable, arguments, workingDirectory, environment}`, all four required; an unknown field is refused. `executable` is `{kind: "tool", tool}` (`tool` is a tool-definition ID that the manifest lists in `requiresTools`, which the host resolves through the layers described in [Toolchains](toolchains.md)) or `{kind: "packaged", path}` (a normalized relative path of a file inside the plugin package). Arguments, the working directory (relative to the workspace root and an existing directory inside it), and the environment have the bounds of [run preparation](../../contract/v1/README.md#provider-run-preparation). -- `temporaryFiles`: at most four `{name, contents}` files of up to 4 MiB of text each. The host writes them into its own temporary directory, removes them when the run ends, and replaces `${temporaryFile:}` in `process.arguments` and `process.environment` with the file's path, so the plugin never needs to know where the host keeps temporary files. A `name` is unique, 1 to 64 letters, digits, `.`, `_`, or `-`, and starts with a letter or digit. +- `temporaryFiles`: at most four files of up to 4 MiB each. `{name, contents}` is text the host writes as UTF-8; `{name, base64}` is bytes in standard base64 the host writes unchanged, without transcoding, for a format whose encoding the plugin must choose (such as a launcher that reads its argument file in the system code page). A file with both or neither is refused at `temporaryFiles[]`. The host writes them into its own temporary directory, removes them when the run ends, and replaces `${temporaryFile:}` in `process.arguments` and `process.environment` with the file's path, so the plugin never needs to know where the host keeps temporary files. A `name` is unique, 1 to 64 letters, digits, `.`, `_`, or `-`, and starts with a letter or digit. - `debug`: `{configurationType, target}`, which makes the run a debug session; see [Debug adapters](debug-adapters.md#starting-a-debug-session-from-a-plugin-backend). `configurationType` is a public debug adapter alias; `target` is `{kind: "connection", port}` (the process listens for a debugger on `port`, 1 to 65535, which the backend puts into the arguments; the host waits for it and attaches the adapter) or `{kind: "launch"}` (the adapter starts the process itself, taking program, arguments, directory, and environment from `process` through its `targetBindings`). - `rerun`: `{function, data}`, one of the plugin's registered functions and its params (a JSON object of up to 64 KiB). When the user chooses Run again, the host invokes the function with `data` as the params and the current `context`, and the backend answers with a new `run/start`, so the backend prepares the restart; without `rerun` the entry cannot be rerun. diff --git a/shared/plugins/reference/domains/build-systems.md b/shared/plugins/reference/domains/build-systems.md index e78592dc..5934223c 100644 --- a/shared/plugins/reference/domains/build-systems.md +++ b/shared/plugins/reference/domains/build-systems.md @@ -45,11 +45,11 @@ | `command`(标准输出) | 运行命令,解码标准输出(`jsonStream` 或规范格式) | `build-command-stdout`(Go) | | `command`(输出文件) | 运行命令,解码它写到 `${outputFile}` 的文件 | `build-command-output-file`(Gradle) | | `provider` | 由插件的构建提供者回答 `lithe/build/model` | [构建提供者](build-providers.md) | -| `hostIntegration` | 交给宿主集成:只限官方插件,目前只有 `maven`,其他是 `plugin.integration.unsupported`。新的构建系统用声明式契约或规范格式,不用它 | — | +| `hostIntegration` | 保留字段:任何 ID 都返回 `plugin.integration.unsupported`。构建系统用声明式契约或规范格式 | — | `command` 来源要声明 `executesProjectCode` 和 `mayUseNetwork`。命令只在宿主按请求启动时运行,静态文件在检测时读取。`map` 只负责选取值,不是转换语言:map 表达不了的(其他文件格式、从工具读出的任务、显式的父子关系、计算出的标签、依赖树)由插件自己的命令或构建提供者输出规范格式 `litheBuildModel`、`litheDependencies`、`litheTestResults` 或 `litheDiagnostics`(`version` 都是 `0`)。不符合解码器的输出是 `plugin.build.invalidOutput`。 -构建系统的 `hostIntegration` 和语言服务器的 `hostIntegration` 是两个不同的声明:前者只限官方插件;后者任何插件都可以声明,见[语言服务器的宿主集成](../../contract/v1/README.md#language-server-host-integrations)。 +构建系统的 `hostIntegration` 和语言服务器的 `hostIntegration` 都是保留字段:Core 不实现任何宿主集成,任何 ID 都返回 `plugin.integration.unsupported`,见[语言服务器的宿主集成](../../contract/v1/README.md#language-server-host-integrations)。 ### 选项、可执行文件与准备任务 @@ -70,7 +70,7 @@ | [Optional read-only model inputs](../../contract/v1/README.md#optional-read-only-model-inputs) | 插件之间共享只读模型 | | [Declarative project preparation](../../contract/v1/README.md#declarative-project-preparation) | 运行前准备任务 | | [Executable resolution](../../contract/v1/README.md#executable-resolution) | 可执行文件候选、排序、严格选择 | -| [Host integrations](../../contract/v1/README.md#host-integrations) | 构建系统的宿主集成(只限官方插件) | +| [Host integrations](../../contract/v1/README.md#host-integrations) | 构建系统的宿主集成(保留字段,Core 不实现) | | [Commands](../../contract/v1/README.md#commands) | `build.*` Core 命令:检测、规划、解码、同步指纹 | ### 示例与用例 @@ -142,11 +142,11 @@ IDs of build systems, tasks, and run entries start with a lowercase letter and u | `command` (stdout) | Run a command and decode its standard output (`jsonStream` or a canonical format) | `build-command-stdout` (Go) | | `command` (output file) | Run a command and decode the file it writes to `${outputFile}` | `build-command-output-file` (Gradle) | | `provider` | The plugin's build provider answers `lithe/build/model` | [Build providers](build-providers.md) | -| `hostIntegration` | Handed to a host integration: official plugins only, currently only `maven`, otherwise `plugin.integration.unsupported`. New build systems use the declarative contract or a canonical format instead | — | +| `hostIntegration` | Reserved: every ID returns `plugin.integration.unsupported`. Build systems use the declarative contract or a canonical format | — | A `command` source declares `executesProjectCode` and `mayUseNetwork`. A command runs only when the host starts it on request; a static file is read during detection. A `map` only selects values and is not a transformation language: whatever a map cannot express (other file formats, tasks read from the tool, explicit parent links, computed tags, dependency trees) is the plugin's own command or build provider printing the canonical `litheBuildModel`, `litheDependencies`, `litheTestResults`, or `litheDiagnostics` (all `version` `0`). Output that does not fit its decoder is `plugin.build.invalidOutput`. -A build system's `hostIntegration` and a language server's `hostIntegration` are two different declarations: the first is limited to official plugins; any plugin may declare the second, see [language server host integrations](../../contract/v1/README.md#language-server-host-integrations). +A build system's `hostIntegration` and a language server's `hostIntegration` are both reserved: Core implements no host integration, and every ID returns `plugin.integration.unsupported`; see [language server host integrations](../../contract/v1/README.md#language-server-host-integrations). ### Options, executables, and preparation tasks @@ -167,7 +167,7 @@ A build system's `hostIntegration` and a language server's `hostIntegration` are | [Optional read-only model inputs](../../contract/v1/README.md#optional-read-only-model-inputs) | Sharing read-only models between plugins | | [Declarative project preparation](../../contract/v1/README.md#declarative-project-preparation) | Tasks that run before a launch | | [Executable resolution](../../contract/v1/README.md#executable-resolution) | Executable candidates, ordering, strict selection | -| [Host integrations](../../contract/v1/README.md#host-integrations) | Host integrations for build systems (official plugins only) | +| [Host integrations](../../contract/v1/README.md#host-integrations) | Host integrations for build systems (reserved; Core implements none) | | [Commands](../../contract/v1/README.md#commands) | `build.*` Core commands: detection, plans, decoding, sync fingerprints | ### Examples and cases diff --git a/shared/plugins/reference/domains/languages.md b/shared/plugins/reference/domains/languages.md index 18eab641..26603a47 100644 --- a/shared/plugins/reference/domains/languages.md +++ b/shared/plugins/reference/domains/languages.md @@ -56,28 +56,16 @@ | `validationArguments` | 候选在使用前必须以状态 0 退出的探针;为空时宿主信任候选,由 LSP 握手决定。 | | `runtimeDependencies` | 宿主要发现并传给服务器的工具,每一项是 `requiresTools` 列出的工具定义 ID。宿主把每个路径导出为 `LITHE__PATH`(用户设置的值优先),并把其目录加到服务器的 `PATH` 前面。项目本地副本不会被选中。 | | `initializationOptions` | 作为 LSP `initializationOptions` 发送的 JSON 对象;其中每个字符串值都是启动模板。 | -| `hostIntegration` | 任何插件都可以声明,见下一节。 | +| `hostIntegration` | 保留字段,见下一节。 | | `requestMethods`、`commands`、`serializedCommands`、`forwardNotifications`、`state`、`remote` | 后端可以向运行中的服务器发什么,以及宿主怎样保存它的状态,见“插件自有会话”。 | 宿主拥有语言服务器进程的生命周期、LSP 会话,以及在提供者被停用时停止进程。 #### 宿主集成(`hostIntegration`) -有些服务器需要数据表达不了的行为,例如从构建文件算出的项目模型、虚拟文档、测试和调试的接线。这些行为是宿主代码,称为宿主集成。服务器按名称选择一个集成,并说明它需要的文件在插件自己包里的哪个位置: +`hostIntegration` 是保留字段。Core 不实现任何宿主集成,也不按语言 ID、服务器 ID 或工具名走特殊逻辑;语言在启动之外需要的一切,都由插件自己的后端、提供者和声明给出。声明任何 `hostIntegration` ID 都返回 `plugin.integration.unsupported`,字段为 `languageServers[].hostIntegration.id`。 -```json -"hostIntegration": { - "id": "jdt", - "capabilities": ["debug", "run", "testing"], - "resources": { "configuration": "language-server/configuration/${arch}", "lombokAgent": "…" } -} -``` - -- 插件拥有文件,宿主拥有行为。宿主只对声明它的服务器启用集成,从不按语言 ID 匹配,也不在包外查找文件。 -- 集成是宿主代码,但只读取声明插件自己包里的资源,交给同一个插件启动的语言服务器或辅助进程。它不授予插件运行自己服务器进程之外的权限。 -- `capabilities` 列出语言服务器的哪些功能由集成提供,取值是 `run`、`testing`、`debug`。没有列出、又没有执行提供者提供的功能,对这种语言不可用。 -- 每个集成定义自己提供的能力和读取的资源。能力未提供、资源未声明、或在执行提供者已经提供的语言上又列出 `run` 或 `testing`,都是 `plugin.manifest.invalid`。 -- 契约目前只定义了 `jdt` 这一个集成 ID:它要求 `runtime` 启动(`java` 工具),`configuration`、`lombokAgent` 为必需资源。宿主没有实现的 ID 返回 `plugin.integration.unsupported`。桌面端源码中目前没有找到 `jdt` 的实现,因此声明它会被拒绝(见“桌面端现状”)。 +声明了某种语言的插件拥有这种语言依赖进程的功能:宿主不再为它运行内置工具,插件缺失或被停用时也不会回退到内置实现。插件的 `executionProviders` 没有提供的功能,对这种语言不可用。 #### 启动模板 @@ -219,7 +207,6 @@ | 语言服务器启动 | 已支持 `packaged`、包内运行时、`systemCommand`(只在 PATH 上查找,依赖项目工具链或用户覆盖的启动计划会被拒绝)和后端应答的命令行。后端应答 `{kind: "tool"}` 在启动时被拒绝,因为工具还没有经过工具链注册表解析 | | 语言服务器呈现 | 只呈现诊断。补全、悬停、跳转、重命名、格式化没有编辑器界面,插件即使提供了也不会显示 | | 插件自有会话与 `languageServer/*` 方法 | 已接通,策略由 Core 执行 | -| `jdt` 宿主集成 | 桌面端源码中未找到实现,声明会收到 `plugin.integration.unsupported` | | 执行提供者(`run`、`test`、`executionProviders`) | 未声明。需要它的插件不会被提供安装 | | 输出链接 | 已支持。Build、Run、Test 控制台的文字变成链接,带 `resolve` 的匹配器在点击时调用后端 | | 内容提供者 | 已支持 `documents/registerContentProvider` | @@ -231,7 +218,7 @@ | [Languages](../../contract/v1/README.md#languages) | 语言 ID、扩展名、文件名、项目标记、依赖路径 | | [Grammars and editor configuration](../../contract/v1/README.md#grammars-and-editor-configuration) | TextMate 语法的上限和拒绝规则、注释与括号配置、在后台运行 | | [Language servers](../../contract/v1/README.md#language-servers) | 启动方式(含 `backend`)、参数、探测参数、运行时依赖 | -| [Language server host integrations](../../contract/v1/README.md#language-server-host-integrations) | 宿主集成与 `jdt` | +| [Language server host integrations](../../contract/v1/README.md#language-server-host-integrations) | 宿主集成(保留字段,Core 不实现) | | [Workspace configuration](../../contract/v1/README.md#workspace-configuration) | `workspaceConfiguration` 的展开与推送 | | [Plugin-owned sessions](../../contract/v1/README.md#plugin-owned-sessions) | 会话字段、会话策略、gate、状态目录与回收、准备状态 | | [Runtime launch](../../contract/v1/README.md#runtime-launch) | 用 `java` 或 `node` 等工具启动包内入口文件 | @@ -319,28 +306,16 @@ The rules for `id`, `fileExtensions`, `fileNames`, `projectFileNames`, and `depe | `validationArguments` | A probe that must exit with status 0 before a candidate is used; empty means the host trusts the candidate and lets the LSP handshake decide. | | `runtimeDependencies` | Tools the host discovers and passes to the server, each a tool-definition ID that `requiresTools` lists. The host exports each path as `LITHE__PATH` (a user-set value takes precedence) and prepends its directory to the server's `PATH`. A project-local copy is never chosen. | | `initializationOptions` | A JSON object sent as the LSP `initializationOptions`. Every string value inside it is a launch template. | -| `hostIntegration` | Any plugin may declare one; see the next section. | +| `hostIntegration` | Reserved; see the next section. | | `requestMethods`, `commands`, `serializedCommands`, `forwardNotifications`, `state`, `remote` | What the backend may ask of the running server and how the host keeps its state; see "Plugin-owned sessions". | The host owns the language server process lifecycle, the LSP session, and stopping the process when the provider is disabled. #### Host integrations (`hostIntegration`) -Some servers need behavior data cannot express: a project model computed from build files, virtual documents, test and debug wiring. That behavior is host code, called a host integration. A server selects one by name and says where the files it needs are inside the plugin's own package: - -```json -"hostIntegration": { - "id": "jdt", - "capabilities": ["debug", "run", "testing"], - "resources": { "configuration": "language-server/configuration/${arch}", "lombokAgent": "…" } -} -``` +`hostIntegration` is reserved. Core implements no host integration and runs no special logic for a language ID, a server ID, or a tool name; everything a language needs beyond its launch comes from the plugin's own backend, providers, and declarations. Any `hostIntegration` ID returns `plugin.integration.unsupported`, at `languageServers[].hostIntegration.id`. -- The plugin owns the files; the host owns the behavior. The host enables an integration only for the server that declares it, never matches on a language ID, and never looks for files outside the package. -- The integration is host code, but it reads only resources inside the declaring plugin's own package and hands them to the language server or helper process that same plugin starts. It grants no permission beyond running the plugin's own server process. -- `capabilities` lists which features the integration provides for the language: `run`, `testing`, or `debug`. A feature that is neither listed nor supplied by an execution provider is unavailable for the language. -- Each integration defines the capabilities it provides and the resources each one reads. A capability it does not provide, a capability whose resource is not declared, or `run` or `testing` for a language whose execution provider already supplies it, is `plugin.manifest.invalid`. -- The contract defines one integration ID, `jdt`: it requires a `runtime` launch with the `java` tool, and `configuration` and `lombokAgent` are required resources. An ID the host does not implement returns `plugin.integration.unsupported`. No implementation of `jdt` was found in the desktop source, so declaring it is refused (see "Desktop status"). +A plugin that declares a language owns its process-backed features: the host stops running its built-in tools for that language, so a missing or disabled plugin never falls back to them. A feature that none of the plugin's `executionProviders` supplies is unavailable for the language. #### Launch templates @@ -482,7 +457,6 @@ Discovery returns `{id: ":workspace", kind: "workspace"}` followed by | Language server launch | Supported for `packaged`, a packaged runtime, `systemCommand` (PATH only; a plan that depends on a project toolchain or a user override is refused), and a backend-answered command line. A backend answer of `{kind: "tool"}` is refused at start, because the tool is not yet resolved through the toolchain registry | | Language server presentation | Only diagnostics are shown. Completion, hover, navigation, rename, and formatting have no editor surface; a plugin that provides them sees no effect | | Plugin-owned sessions and `languageServer/*` methods | Wired; Core enforces the policy | -| `jdt` host integration | No implementation found in the desktop source; declaring it is refused with `plugin.integration.unsupported` | | Execution providers (`run`, `test`, `executionProviders`) | Not declared. A plugin that needs them is not offered for installation | | Output links | Supported. Console text in Build, Run, and Test becomes links; a matcher with `resolve` calls the backend on click | | Content providers | `documents/registerContentProvider` supported | @@ -494,7 +468,7 @@ Discovery returns `{id: ":workspace", kind: "workspace"}` followed by | [Languages](../../contract/v1/README.md#languages) | Language IDs, extensions, file names, project markers, dependency paths | | [Grammars and editor configuration](../../contract/v1/README.md#grammars-and-editor-configuration) | TextMate grammar limits and refusal rules, comment and bracket configuration, background execution | | [Language servers](../../contract/v1/README.md#language-servers) | Launch kinds (including `backend`), arguments, probe arguments, runtime dependencies | -| [Language server host integrations](../../contract/v1/README.md#language-server-host-integrations) | Host integrations and `jdt` | +| [Language server host integrations](../../contract/v1/README.md#language-server-host-integrations) | Host integrations (reserved; Core implements none) | | [Workspace configuration](../../contract/v1/README.md#workspace-configuration) | Expanding and pushing `workspaceConfiguration` | | [Plugin-owned sessions](../../contract/v1/README.md#plugin-owned-sessions) | Session fields, session policy, gates, state directories and pruning, preparation status | | [Runtime launch](../../contract/v1/README.md#runtime-launch) | Starting a package entry file with a tool such as `java` or `node` | diff --git a/shared/plugins/reference/domains/manifest.md b/shared/plugins/reference/domains/manifest.md index 1571fcdf..f800e0ec 100644 --- a/shared/plugins/reference/domains/manifest.md +++ b/shared/plugins/reference/domains/manifest.md @@ -27,7 +27,7 @@ | `ui` | `ui` | [界面](ui.md) | | `buildSystems`(不含 `provider`) | `buildSystems`;`modelSharing` 与 `modelInputs` 还需要 `buildModelInputs`(桌面端未声明) | [声明式构建](build-systems.md) | | `buildSystems[].provider` | `buildProviders` | [构建提供者](build-providers.md) | -| `buildSystems` 里的 `hostIntegration`(模型、测试结果、依赖来源) | `buildSystems`;只限官方插件,目前只有 `maven` | [声明式构建](build-systems.md) | +| `buildSystems` 里的 `hostIntegration` | 保留字段:任何 ID 都返回 `plugin.integration.unsupported` | [声明式构建](build-systems.md) | | `vcsProviders` | `vcsProviders`(桌面端未声明) | [VCS](vcs.md) | | `backend` | `backend` | [插件后端](backend.md) | | `pluginFunctions` | `pluginFunctions`、`backend`;不授予权限 | [插件后端](backend.md) | @@ -41,7 +41,7 @@ `requires` 必须写,并且恰好列出清单用到的能力:漏列或多列都是 `plugin.manifest.invalid`。能力名不是契约定义的,或版本不是 `0`,是 `plugin.api.unsupported`,`field` 为 `requires.<能力名>`。 -`hostIntegration` 在两处是不同的声明。语言服务器的 `hostIntegration` 对所有插件开放,只读取声明插件自己包里的资源,不授予额外权限(见[语言](languages.md))。构建系统的 `hostIntegration` 仍只限官方插件,目前只有 `maven`(见[声明式构建](build-systems.md))。宿主没有实现的 ID 一律返回 `plugin.integration.unsupported`。 +语言服务器和构建系统都有 `hostIntegration` 字段,两处都是保留字段:Core 不实现任何宿主集成,声明任何 ID 都返回 `plugin.integration.unsupported`(见[语言](languages.md)和[声明式构建](build-systems.md))。语言或构建工具需要的一切,都由插件自己的后端、提供者和声明给出。 ### 能力与宿主 @@ -139,7 +139,7 @@ Every plugin package has a `plugin.json` at its root. It says who the plugin is, | `ui` | `ui` | [UI](ui.md) | | `buildSystems` (without `provider`) | `buildSystems`; `modelSharing` and `modelInputs` also need `buildModelInputs` (not declared by the desktop host) | [Declarative build systems](build-systems.md) | | `buildSystems[].provider` | `buildProviders` | [Build providers](build-providers.md) | -| `hostIntegration` inside `buildSystems` (model, test-result, or dependency source) | `buildSystems`; official plugins only, and only `maven` exists | [Declarative build systems](build-systems.md) | +| `hostIntegration` inside `buildSystems` | Reserved: every ID returns `plugin.integration.unsupported` | [Declarative build systems](build-systems.md) | | `vcsProviders` | `vcsProviders` (not declared by the desktop host) | [VCS](vcs.md) | | `backend` | `backend` | [Plugin backend](backend.md) | | `pluginFunctions` | `pluginFunctions`, `backend`; grants no permission | [Plugin backend](backend.md) | @@ -153,7 +153,7 @@ Every plugin package has a `plugin.json` at its root. It says who the plugin is, `requires` is required and must list **exactly** the capabilities the manifest uses: a missing or unused one is `plugin.manifest.invalid`. A capability name the contract does not define, or a version other than `0`, is `plugin.api.unsupported` with `field` set to `requires.`. -`hostIntegration` is two different declarations. A language server's `hostIntegration` is open to every plugin; it reads only resources inside the declaring plugin's own package and grants no extra permission (see [Languages](languages.md)). A build system's `hostIntegration` is still limited to official plugins, and only `maven` exists (see [Declarative build systems](build-systems.md)). An ID the host does not implement is always `plugin.integration.unsupported`. +Language servers and build systems each have a `hostIntegration` field, and both are reserved: Core implements no host integration, and declaring any ID returns `plugin.integration.unsupported` (see [Languages](languages.md) and [Declarative build systems](build-systems.md)). Everything a language or build tool needs comes from the plugin's own backend, providers, and declarations. ### Capabilities and host diff --git a/shared/plugins/reference/protocol-changelog.md b/shared/plugins/reference/protocol-changelog.md index b0c1dab3..f982681c 100644 --- a/shared/plugins/reference/protocol-changelog.md +++ b/shared/plugins/reference/protocol-changelog.md @@ -125,9 +125,16 @@ | API-35 | `functions/invoke` | 0 | 2026-11-11 | [#17](https://github.com/Lithe-IDEA/Lithe-core-rust/pull/17) | | — | `keymapImport/listInputs` | 0 | 2026-11-11 | [#17](https://github.com/Lithe-IDEA/Lithe-core-rust/pull/17) | | — | `keymapImport/readChunk` | 0 | 2026-11-11 | [#17](https://github.com/Lithe-IDEA/Lithe-core-rust/pull/17) | +| API-23 | `run/start`:`temporaryFiles` 增加 `{name, base64}` 字节文件 | 0 | 2026-11-13 | 待补(本 PR) | ## 时间线(新的在上) +### 2026-11-13 + +| PR | 修改人 | API 编号 | 协议 / 能力 | 改了什么 | +| --- | --- | --- | --- | --- | +| 待补(本 PR) | [@xiaoyumuxi](https://github.com/xiaoyumuxi) | API-23 | `pluginAPIVersion` 0 内的补充 | `run/start` 的 `temporaryFiles` 可写 `{name, base64}` 字节文件,宿主原样写入、不转码,供编码必须由插件决定的格式使用(例如按系统代码页读取的参数文件);`hostIntegration` 明确为保留字段:Core 不实现任何宿主集成,任何 ID 都以 `plugin.integration.unsupported` 拒绝,清单 schema 去掉只含 `jdt`、`maven` 的枚举;项目生成器的 `${field..major}` 取版本的第一个数字段,不再把 `1.8.0_412` 特判成 `8`;契约去掉 Java、Maven、JDT 专属说明。 | + ### 2026-11-11 | PR | 修改人 | API 编号 | 协议 / 能力 | 改了什么 | From 159e0f6b40a8d4ac299938801851e86f4ef1d388 Mon Sep 17 00:00:00 2001 From: Yao Jingxi <23722032@bjtu.edu.cn> Date: Fri, 13 Nov 2026 23:33:11 +0800 Subject: [PATCH 2/5] =?UTF-8?q?docs(plugins):=20=E5=8D=8F=E8=AE=AE?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=AE=B0=E5=BD=95=E8=A1=A5=E4=B8=8A=20#23?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/plugins/reference/protocol-changelog.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared/plugins/reference/protocol-changelog.md b/shared/plugins/reference/protocol-changelog.md index f982681c..1c44dd0a 100644 --- a/shared/plugins/reference/protocol-changelog.md +++ b/shared/plugins/reference/protocol-changelog.md @@ -125,7 +125,7 @@ | API-35 | `functions/invoke` | 0 | 2026-11-11 | [#17](https://github.com/Lithe-IDEA/Lithe-core-rust/pull/17) | | — | `keymapImport/listInputs` | 0 | 2026-11-11 | [#17](https://github.com/Lithe-IDEA/Lithe-core-rust/pull/17) | | — | `keymapImport/readChunk` | 0 | 2026-11-11 | [#17](https://github.com/Lithe-IDEA/Lithe-core-rust/pull/17) | -| API-23 | `run/start`:`temporaryFiles` 增加 `{name, base64}` 字节文件 | 0 | 2026-11-13 | 待补(本 PR) | +| API-23 | `run/start`:`temporaryFiles` 增加 `{name, base64}` 字节文件 | 0 | 2026-11-13 | [#23](https://github.com/Lithe-IDEA/Lithe-core-rust/pull/23) | ## 时间线(新的在上) @@ -133,7 +133,7 @@ | PR | 修改人 | API 编号 | 协议 / 能力 | 改了什么 | | --- | --- | --- | --- | --- | -| 待补(本 PR) | [@xiaoyumuxi](https://github.com/xiaoyumuxi) | API-23 | `pluginAPIVersion` 0 内的补充 | `run/start` 的 `temporaryFiles` 可写 `{name, base64}` 字节文件,宿主原样写入、不转码,供编码必须由插件决定的格式使用(例如按系统代码页读取的参数文件);`hostIntegration` 明确为保留字段:Core 不实现任何宿主集成,任何 ID 都以 `plugin.integration.unsupported` 拒绝,清单 schema 去掉只含 `jdt`、`maven` 的枚举;项目生成器的 `${field..major}` 取版本的第一个数字段,不再把 `1.8.0_412` 特判成 `8`;契约去掉 Java、Maven、JDT 专属说明。 | +| [#23](https://github.com/Lithe-IDEA/Lithe-core-rust/pull/23) | [@xiaoyumuxi](https://github.com/xiaoyumuxi) | API-23 | `pluginAPIVersion` 0 内的补充 | `run/start` 的 `temporaryFiles` 可写 `{name, base64}` 字节文件,宿主原样写入、不转码,供编码必须由插件决定的格式使用(例如按系统代码页读取的参数文件);`hostIntegration` 明确为保留字段:Core 不实现任何宿主集成,任何 ID 都以 `plugin.integration.unsupported` 拒绝,清单 schema 去掉只含 `jdt`、`maven` 的枚举;项目生成器的 `${field..major}` 取版本的第一个数字段,不再把 `1.8.0_412` 特判成 `8`;契约去掉 Java、Maven、JDT 专属说明。 | ### 2026-11-11 From 5a8e67d4b73f5e633e67dc09e722c3b22b47b280 Mon Sep 17 00:00:00 2001 From: Yao Jingxi <23722032@bjtu.edu.cn> Date: Fri, 13 Nov 2026 23:39:39 +0800 Subject: [PATCH 3/5] =?UTF-8?q?test(run):=20=E5=A5=91=E7=BA=A6=20fixture?= =?UTF-8?q?=20=E8=A6=86=E7=9B=96=20temporaryFiles=20base64=20=E7=9A=84?= =?UTF-8?q?=E6=8B=92=E7=BB=9D=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit run-start.json 增加三条拒绝用例:同时给出 contents 和 base64、两者都不给、 base64 不是标准编码。Core 的 fixture 回放测试改为传入临时目录, 让这些用例走到逐项校验,而不是停在缺少临时目录。 --- rust/lithe-core/src/tests/plugin_run_debug.rs | 4 ++ .../contract/v1/fixtures/run/run-start.json | 44 ++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/rust/lithe-core/src/tests/plugin_run_debug.rs b/rust/lithe-core/src/tests/plugin_run_debug.rs index f9af00b5..02bbe5a7 100644 --- a/rust/lithe-core/src/tests/plugin_run_debug.rs +++ b/rust/lithe-core/src/tests/plugin_run_debug.rs @@ -112,12 +112,16 @@ fn run_start_fixture_cases_decode_the_same_in_every_host() { .map(|directory| directory.as_str().unwrap()) .collect(); let workspace = Workspace::new("run-start-fixture", &directories); + // Every host supplies its temporary directory, so the temporary-file refusals of the + // fixture are reached instead of stopping at a missing directory. + let temporary = Workspace::new("run-start-fixture-files", &[]); let manifest = named_manifest(&fixture); for case in fixture["cases"].as_array().unwrap() { let name = case["name"].as_str().unwrap(); let outcome = run_decode_start_command(json!({ "manifest": manifest, "workspaceRoot": workspace.root(), + "temporaryDirectory": temporary.root(), "params": case["params"], })) .expect("payload is well formed"); diff --git a/shared/plugins/contract/v1/fixtures/run/run-start.json b/shared/plugins/contract/v1/fixtures/run/run-start.json index 50228afe..a779efb5 100644 --- a/shared/plugins/contract/v1/fixtures/run/run-start.json +++ b/shared/plugins/contract/v1/fixtures/run/run-start.json @@ -1,5 +1,5 @@ { - "description": "Parameters of lithe/host/run/start and what run.decodeStart returns. The host passes the plugin's manifest (`manifest`, a file in this directory), a workspace root that holds the directories in `workspaceDirectories`, and the request's `params`. `start` is the normalized start; `error` is the rejection every host reports. Cases with temporary files depend on the host's temporary directory and are covered by Core's own tests.", + "description": "Parameters of lithe/host/run/start and what run.decodeStart returns. The host passes the plugin's manifest (`manifest`, a file in this directory), a workspace root that holds the directories in `workspaceDirectories`, and the request's `params`. `start` is the normalized start; `error` is the rejection every host reports. The host supplies an absolute temporary directory (`temporaryDirectory`) with every case. Accepted starts with temporary files return paths inside that directory, so only their refusals are listed here; the accepted ones are covered by Core's own tests.", "manifest": "tool-reference-launch.plugin.json", "workspaceDirectories": ["app", "app/tools"], "cases": [ @@ -266,6 +266,48 @@ }, "error": { "code": "plugin.run.invalidStart", "field": "temporaryFiles" } }, + { + "name": "a temporary file with both text and bytes", + "params": { + "label": "Run", + "process": { + "executable": { "kind": "tool", "tool": "interpreter" }, + "arguments": [], + "workingDirectory": ".", + "environment": {} + }, + "temporaryFiles": [{ "name": "args", "contents": "", "base64": "" }] + }, + "error": { "code": "plugin.run.invalidStart", "field": "temporaryFiles[0]" } + }, + { + "name": "a temporary file with neither text nor bytes", + "params": { + "label": "Run", + "process": { + "executable": { "kind": "tool", "tool": "interpreter" }, + "arguments": [], + "workingDirectory": ".", + "environment": {} + }, + "temporaryFiles": [{ "name": "args" }] + }, + "error": { "code": "plugin.run.invalidStart", "field": "temporaryFiles[0]" } + }, + { + "name": "temporary file bytes that are not standard base64", + "params": { + "label": "Run", + "process": { + "executable": { "kind": "tool", "tool": "interpreter" }, + "arguments": [], + "workingDirectory": ".", + "environment": {} + }, + "temporaryFiles": [{ "name": "args", "base64": "not base64!" }] + }, + "error": { "code": "plugin.run.invalidStart", "field": "temporaryFiles[0].base64" } + }, { "name": "an unknown start parameter", "params": { From 443c8a42c762296587612d0fd00c019cf5111d17 Mon Sep 17 00:00:00 2001 From: Yao Jingxi <23722032@bjtu.edu.cn> Date: Sat, 14 Nov 2026 00:10:37 +0800 Subject: [PATCH 4/5] =?UTF-8?q?docs(contracts):=20Core=20=E4=BD=9C?= =?UTF-8?q?=E4=B8=BA=E5=BA=94=E7=94=A8=E5=A5=91=E7=BA=A6=E5=94=AF=E4=B8=80?= =?UTF-8?q?=E7=9C=9F=E6=BA=90=EF=BC=8C=E8=A1=A5=E5=9B=9E=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E8=A1=A8=E4=B8=8E=20AI=20=E9=85=8D=E7=BD=AE=E7=AB=A0=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rust-core-api.md 补回 #19 误删的命令总表(375 条,每个 CoreCommand::parse 接受的命令一行) 和 AI configuration 章节;Build systems 表补上 build.resolveStoredOptions、build.editStoredOption。 - protocol::command 新增测试:命令表与解析器逐条比对,增删改命令必须同一次提交更新表格。 - 删除 rust/fixtures/i18n(归 Lithe-IDEA i18n 生成器所有,迁到 shared/i18n/fixtures) 和无人读取的 rust/fixtures/phpunit-project。 Refs 1lck/Lithe-IDEA#1853 --- rust/contracts/application-boundary.md | 2 +- rust/contracts/rust-core-api.md | 407 +++++++++++++++++- rust/fixtures/i18n/format-v1.json | 23 - rust/fixtures/i18n/native-abi-v1.json | 11 - rust/fixtures/phpunit-project/.gitignore | 3 - rust/fixtures/phpunit-project/composer.json | 5 - rust/fixtures/phpunit-project/phpunit.xml | 1 - rust/fixtures/phpunit-project/src/Store.php | 6 - .../phpunit-project/tests/StoreTest.php | 12 - rust/lithe-core/src/protocol/command.rs | 72 ++++ 10 files changed, 479 insertions(+), 63 deletions(-) delete mode 100644 rust/fixtures/i18n/format-v1.json delete mode 100644 rust/fixtures/i18n/native-abi-v1.json delete mode 100644 rust/fixtures/phpunit-project/.gitignore delete mode 100644 rust/fixtures/phpunit-project/composer.json delete mode 100644 rust/fixtures/phpunit-project/phpunit.xml delete mode 100644 rust/fixtures/phpunit-project/src/Store.php delete mode 100644 rust/fixtures/phpunit-project/tests/StoreTest.php diff --git a/rust/contracts/application-boundary.md b/rust/contracts/application-boundary.md index 5d41ea90..4758c4c9 100644 --- a/rust/contracts/application-boundary.md +++ b/rust/contracts/application-boundary.md @@ -27,7 +27,7 @@ no runtime JSON loading, package download or write to installed resources occurs The locale registry preserves each platform's persisted identifiers and default language. Explicit fallback chains resolve shared entries during generation; all existing platform catalogs and presentation-owned bilingual copy use this source. Translation coverage is not a -capability claim. [Format fixtures](../fixtures/i18n/format-v1.json) bind generation +capability claim. [Format fixtures](https://github.com/1lck/Lithe-IDEA/blob/preview/shared/i18n/fixtures/format-v1.json) bind generation to native positional formatting, literal percent signs and unmodified argument data. ## Feature Contracts diff --git a/rust/contracts/rust-core-api.md b/rust/contracts/rust-core-api.md index 37636689..0d6cd826 100644 --- a/rust/contracts/rust-core-api.md +++ b/rust/contracts/rust-core-api.md @@ -273,7 +273,7 @@ The ACP Agent ABI and the `agent.*` commands were removed together with the Agent conversation feature. `lithe_agent_open_json`, `lithe_agent_send_json` and `lithe_agent_close` no longer exist, and Core starts no adapter, proxies no model call and keeps no conversation history. Rust Core keeps only deterministic -AI configuration parsing (`ai.configuration.*`) plus MCP configuration planning. +AI configuration parsing (`ai.configuration.*`). The removed ABI text, fixtures and the `lithe-agent-host` crate remain available in this repository's history. @@ -341,6 +341,388 @@ subsystem: `mcp.parseConfiguration`, `mcp.parseRegistry`, `mcp.featuredRegistry` server (`lithe-mcp` plus `rust/fixtures/ide-api/mcp-protocol.json`) is unaffected and stays in `lithe-ide-host`. +Every command name `CoreCommand::parse` accepts has exactly one row below. The +`protocol::command` tests compare this table with the parser, so adding, renaming +or removing a command fails until the table changes in the same commit. + +| Command | Purpose | +| --- | --- | +| `core.ping` | Verify the ABI and protocol version | +| `ideHost.control` | Control the native IDE capability broker | +| `ai.commitInputBudget` | Budget diff prefixes before crossing the native boundary | +| `ai.planCommit` | Plan a credential-free commit message request | +| `ai.decodeCommitMessage` | Decode a provider commit message response | +| `ai.configuration.sources` | Name the native files of one AI CLI configuration source | +| `ai.configuration.parse` | Parse AI CLI configuration into credential-free metadata | +| `ai.pullRequest.plan` | Plan a credential-free pull request description request | +| `ai.pullRequest.decode` | Decode a strict pull request title and description | +| `ai.inlineEdit.plan` | Plan a credential-free inline edit request | +| `ai.inlineEdit.decode` | Decode the replacement text of an inline edit response | +| `plugins.declarative.validate` | Validate and normalize a declarative plugin manifest | +| `plugins.nativeUI.validateData` | Validate native plugin UI data with declaration identity and node budgets | +| `plugins.web.open` | Register a plugin page for a web tool window | +| `plugins.web.close` | Forget a plugin page and lists its calls to cancel | +| `plugins.web.resource` | Decide how a plugin page request is served | +| `plugins.web.document` | Prepend the policy and bridge to a page document | +| `plugins.web.request` | Check one bridge message of a plugin page | +| `plugins.web.reply` | Frame the host's answer to a bridge action | +| `plugins.web.event` | Frame an event for a plugin page | +| `plugins.declarative.runPlan` | Expand a declarative plugin's run template for one workspace file | +| `plugins.declarative.discoverTests` | List a declarative plugin's workspace and file test items | +| `plugins.declarative.testPlan` | Expand a declarative plugin's test template for a workspace, file, or test case | +| `plugins.declarative.languageServerLaunchPlan` | Expand a declarative plugin's language-server, build-provider, VCS-provider, backend, or debug-adapter launch with host directories and architecture | +| `plugins.languageServers.decodeLaunch` | Bound the answer of a plugin backend that resolved a language server's launch and return the plan the host starts | +| `plugins.languageServers.stateRetention` | Choose which inactive per-workspace state directories of a plugin language server the host deletes | +| `plugins.contentProviders.decode` | Bound the answer of a plugin content provider function and return the document text | +| `debug.adapters.startArguments` | Check one debug start against a plugin's `debugAdapters[]` entry and return its DAP launch arguments | +| `debug.validateAdapterEndpoint` | Validate an IDE-to-DAP TCP endpoint and classify whether remote consent is required; performs no DNS or network I/O | +| `debug.adapters.executablePlan` | Say where to look for the program a launch debugs: the user's setting and the adapter's candidate paths | +| `debug.transport.policy` | Return the default byte-stream deadlines, queue budgets and the session `initialize` deadline; creates no state | +| `debug.transport.handle` | Apply one platform event to an IDE-to-adapter byte-stream lifecycle and return the effects the platform runs next | +| `debug.transport.release` | Forget the lifecycle of a stopped byte stream once the platform dropped its handles | +| `plugins.declarative.resolveSettings` | Merge a plugin's stored global and project setting layers into effective values with their source | +| `plugins.settings.catalog` | Project enabled plugin settings pages from installed manifests | +| `plugins.settings.navigation` | Project searched settings navigation without platform policy | +| `plugins.settings.delivery` | Route committed preference effects using native ownership facts | +| `plugins.declarative.settingsState` | Read complete plugin preference documents into Core-normalized rows, sources, and bounded native read plans | +| `plugins.declarative.settingsActionPlan` | Resolve an installed settings action and its native confirmation requirement | +| `plugins.installed.select` | Select numeric installed versions from native facts without reading paths | +| `plugins.declarative.editSettings` | Validate one atomic setting edit batch and return replacement text while preserving unrelated plugins and dormant values | +| `plugins.declarative.workspaceConfiguration` | Expand a language server's `workspaceConfiguration` with the merged setting values | +| `plugins.declarative.decodeGrammar` | Validate independent static editor resources | +| `plugins.editorActions.decode` | Validate independent action registration, collection or click results | +| `plugins.hostApi.describe` | Describe backend host methods and their required grants from Core's method table | +| `plugins.hostApi.authorize` | Check a method, service and current session grants using Core's capability rules; the host still checks session and resource ownership | +| `plugins.editorProviders.decode` | Validate editor provider registrations and provider function results | +| `keymap.decodeImport` | Validate a normalized shortcut import against the host's action catalog | +| `keymap.planImportSources` | Plan the selected provider's declared shortcut source inputs | +| `keymap.planImportBindings` | Plan conflicts and default-key displacement for a shortcut import | +| `debug.startOrContinue` | Decide whether a combined debug shortcut starts, continues or does nothing | +| `toolchains.registry` | Revalidate complete enabled plugin manifests, `{plugins:[{manifest}]}`, into a deterministic tool-definition/provider registry; the host supplies no origin evidence. A short tool ID defined by several enabled plugins belongs to none of them: none of those definitions is registered, the rest register as usual, and `conflicts: [{tool, pluginIDs}]` (sorted by tool, `pluginIDs` sorted) reports each contested tool. A repeated plugin ID rejects the batch. No native host capability claim | +| `toolchains.decodeReply` | Validate an inert toolchain query/reply pair against bound provider identity, generation, declared tools and host-native filesystem evidence; no transport or native I/O | +| `toolchains.failureReply` | Build the JSON-RPC error reply for a query that missed its deadline or found no backend, so the host passes it to `toolchains.decodeReply` like a provider's own error; cancellation has none | +| `toolchains.resolvePins` | Validate a captured local document and find the nearest ancestor directory pin, retaining dormant and legacy entries without probing or writing | +| `toolchains.editPins` | Apply one bounded batch of directory pin edits to a captured local document without writing native files | +| `toolchains.pinStoragePlan` | Declare bounded workspace-native pin and ignore sources | +| `toolchains.decodePinDocument` | Validate a complete native source or supply the absent-source default | +| `toolchains.editPinSources` | Plan deterministic pin and Git ignore source edits without native I/O | +| `toolchains.resolveConsumerPreferences` | Validate an inert consumer preference document and resolve one tool's explicit Run, consumer, directory or workspace choice while keeping workspace/consumer inheritance symbolic; no native files or launch authority | +| `toolchains.editConsumerPreferences` | Edit or reset one exact consumer/directory tool preference; return the document and effective input without native writes | +| `toolchains.resolve` | Resolve the fixed/project/global/automatic layers from one generation-bound snapshot, retaining unmet requirements, unavailable providers with their cause, conflicts and terminal fixed-value failures | +| `toolchains.checkVersion` | Match captured output with an enabled plugin definition's version expression and compare generic numeric components with an optional minimum; never starts a probe | +| `toolchains.discoveryPlan` | Return platform-filtered definition-owned scan declarations, layouts, probes, PATH entries, login-shell home variables and budgets; never scans native directories | +| `toolchains.candidates` | List one tool's login-shell homes and host-scanned installations with checked executables, versions or skip reasons, and the automatic-layer choice; never starts a probe | +| `toolchains.instantiateLaunch` | Expand an enabled tool definition's `launch` template for a selected executable, entry, runtime arguments and arguments; a part the template cannot carry is `plugin.toolchain.launchMismatch`; performs no I/O | +| `run.decodeStart` | Validate a plugin backend's `lithe/host/run/start` params against its manifest and the workspace, and return the normalized start with temporary-file paths substituted | +| `debug.decodeBackendConfiguration` | Validate a plugin backend's `lithe/host/debug/start` params against its manifest and return the qualified adapter, request, label and configuration | +| `outputLinks.match` | Find the links plugin `outputLinkMatchers` declare in Run or Test console lines, as UTF-16 ranges with candidate paths or a backend function to resolve them | +| `outputLinks.decodeResolution` | Check a matcher's `resolve` function reply and return the 1-based file location to open, or `null` | +| `build.detect` | Find every build-system instance the enabled plugins declare in a workspace, with models that need no process | +| `build.projectOptions` | Validate a native team's persisted Build options | +| `build.resolveStoredOptions` | Merge checked team and machine-local Build preferences without I/O | +| `build.editStoredOption` | Edit one declared option in a machine-local Build document without I/O | +| `build.invocationPlan` | List a build system's executable candidates, runtime dependencies, and version probe for one platform | +| `build.checkToolVersion` | Decide whether a build tool's or runtime's version output meets the declared minimum | +| `build.resolveOptions` | Check stored build options against the declaration and return effective values, choices, and the fingerprint | +| `build.modelPlan` | Expand the command that prints a command-sourced build model, or the request a build provider answers | +| `build.taskPlan` | Expand one declared or model task for a workspace, project, or target | +| `build.testPlan` | Plan a test run for a scope, suite, or case and say where its reports will be | +| `build.prepareDebugPlan` | Plan same-plugin native debug preparation | +| `build.decodeDebugPreparation` | Validate an existing instance-contained native debug program and bounded process facts | +| `build.resolveModelInputs` | Validate enabled producer facts and project a bounded public model snapshot for a declared consumer | +| `build.configurationPlan` | Plan a read of the provider's non-sensitive native configuration | +| `build.configurationEditPlan` | Plans format-independent provider editing into private host staging | +| `build.decodeConfigurationEdit` | Validates the receipt without native document bytes | +| `build.decodeConfiguration` | Validate native source identities, raw/effective values and XML bindings | +| `build.prepareRunPlan` | Plan Provider-owned application/service/task preparation from a model target | +| `build.decodeRunPreparation` | Validate portable launch data before the native Run host starts a process | +| `build.testDiscoveryPlan` | Plan opt-in Provider test discovery for an instance | +| `build.decodeTestDiscovery` | Validate, deduplicate and order Provider test selectors | +| `build.dependencyPlan` | Expand the command that prints a project's dependency tree, or the request a build provider answers | +| `build.decodeModel` | Decode model output or a provider result into projects, targets, tasks, content roots, option values, and run entries | +| `build.decodeTestResults` | Decode a test run's reports and output into per-case results and a summary | +| `build.decodeDiagnostics` | Decode build output into workspace file diagnostics | +| `build.decodeDependencies` | Decode dependency output or a provider result into one project's bounded tree | +| `build.syncFingerprint` | Digest the files a build model depends on, to tell whether it is stale | +| `build.resolveTestRunOutcome` | Reduce a test run's termination, selected scope and decoded summary to one tool-neutral verdict | +| `build.mergeTestOutcomes` | Merge a run's test result ids into the stored history inside the host-stated id/prefix scope | +| `vcs.detectRoots` | Find the VCS roots of a workspace from static markers, with registered providers, ambiguous directories, and applied mappings | +| `vcs.ownerOf` | Assign workspace paths to the detected root that owns each | +| `vcs.decodeReply` | Validate a VCS provider's reply and return the normalized result | +| `project.generatorCatalog` | List the project generators of the given plugins, with the plugins that provide each requirement | +| `project.generatorForm` | Evaluate a generator's form: derived defaults, visibility, problems, and active requirements | +| `project.generationPlan` | Expand a filled-in form into the files, commands, and archive downloads that create a project | +| `project.checkName` | Check that a name can be a new project's directory name | +| `project.extractArchive` | Extract a downloaded project archive into the staging directory, refusing unsafe entries | +| `plugins.marketplace.validateIndex` | Validate a marketplace index and report each plugin's availability for one platform | +| `plugins.marketplace.resolveSource` | Resolve a user-entered marketplace source to the index URL to fetch | +| `plugins.marketplace.installReview` | List a package's permissions and programs, and those not yet approved | +| `community.discourse.auth.begin` | Create an ephemeral RSA-OAEP authorization session and return the Discourse browser URL | +| `community.discourse.auth.complete` | Decrypt, validate, and consume one Discourse user API key callback | +| `community.discourse.topics` | List normalized latest or top topic summaries | +| `community.discourse.topic` | Read one topic with ordered, sanitized post HTML | +| `community.discourse.categories` | List normalized visible categories | +| `community.discourse.search` | Search normalized topics and sanitized posts | +| `community.discourse.auth.revoke` | Revoke the current Discourse user API key | +| `workspace.snapshot` | Enumerate visible workspace nodes and relative file paths; `nestedWorktrees` lists linked worktrees checked out inside their own repository, which are left out of the tree, files, and search index; the built-in hidden names are skipped without being read, and include `node_modules`, Python virtual environments (`.venv`, `venv`), `__pycache__` and `*.pyc` files; `includeTree: false` returns only `files` and a childless `root`; `files` (and the tree, when included) keep the lowercase traversal order that bounded consumers such as Windows Quick Open rely on, not the natural display order of `workspace.listDirectories` | +| `workspace.releaseIndexing` | Retire a policy and clear index/cache data at project close; accepts `root`, optional `ownerId`, and that owner's last observed `revision`, returns `released`; an old close must not retire a new generation | +| `workspace.configureIndexing` | Install a process-local, canonical-root indexing policy before workspace consumers run; returns a monotonic revision, effective directory reasons and Git failure state; see `rust/fixtures/workspace/indexing-policy-v1.json` | +| `workspace.refreshIndexing` | Refresh external Git decisions for a live `root`, `ownerId`, `revision` lease without changing preferences or release tokens; returns lazy `IndexingStatus` | +| `workspace.filterChanges` | Filter an array of workspace-relative paths, or `{ paths, hiddenFilePatterns }` whose `!` entries turn default exclusions off, against Core's default build/cache exclusions; preserve order, spelling, and the empty root path. Native event loss bypasses filtering. Hosts relativize to the most specific explicit watch root and retain explicitly watched files | +| `workspace.listDirectories` | Return one visible level of children for each requested workspace-relative directory (`""` is the root); missing, hidden, symlinked, or nested-worktree paths are omitted, and each listing reports the nested worktrees it left out. Child directories have `children: []` only when a shallow probe proves them visibly empty; otherwise children remain omitted (unloaded), with no recursive tree allocation. Children list directories first, then names in IDEA's natural file-name order (digit runs compare by value, case-insensitive, `-` next to `_`; fixture `rust/fixtures/workspace/file-name-order-v1.json`) | +| `workspace.orderEntries` | Order natively enumerated directory siblings | +| `workspace.rankNames` | Pure ranking of candidate names that the platform enumerates itself (`{ query, candidates: [{ name, path? }], context: { openPaths, recentPaths }, limitPerTier? }`). Use it only for lists Core cannot index: Windows remote/WSL roots, symbol lists, navigator lists. Roots Core indexes go through `workspace.searchEverywhere`, so an inventory is never sent back to Core. Returns `matches: [{ index, degree, tier, nameMatchRanges }]` ordered by degree, open/recent tier, recency, then file-name order (fixture `rust/fixtures/search/rank-names-v1.json`) | +| `workspace.planDuplicate` | Name the copy of one project item next to its source (`foo.txt` → `foo copy.txt`, then `foo copy 2.txt`) from the sibling names the host has seen to exist | +| `workspace.planPaste` | Name pasted project items in one destination directory: a free name is kept, a taken one gets the copy suffix, and earlier items of the same paste count as taken | +| `workspace.reduceDirectoryAction` | Apply "set role", "exclude" or "cancel exclusion" to a root's persisted directory rules and return the complete state to persist | +| `workspace.relocateDirectoryRules` | Move (or drop, after a delete) the directory rule keys of a renamed, moved or deleted subtree, optionally into another workspace root's rules | +| `workspace.repositories` | Discover deterministic Git repository roots for an opened workspace | +| `workspace.recentProjects.reduce` | Apply one record, remove, pin, import, or normalize action to the recent project list and return the retained order | +| `workspace.searchIndex.warm` | Build or reuses the workspace search index | +| `workspace.searchIndex.update` | Apply changed paths to the search index | +| `workspace.searchIndex.invalidate` | Drop a workspace's cached search index | +| `search.actions` | Match host-localized action text without workspace I/O | +| `workspace.search` | Search visible file names and UTF-8 text files | +| `workspace.searchEverywhere` | Search file paths, contents, and symbols | +| `workspace.replacePreview` | Return deterministic replacement lines and complete replacement text | +| `workspace.replaceText` | Plan replacement against a host-captured text snapshot without writing | +| `workspace.searchTextBatch` | Search provider-captured text snapshots using workspace matching rules | +| `file.read` | Read a text file using a workspace-relative path; optional `encoding`, see [File encodings](#file-encodings) | +| `file.write` | Write a text file using a workspace-relative path; optional `encoding` and `bom` | +| `text.decode` | Decode `{ bytes }` (standard base64 of bytes the host read) like `file.read`, optional `encoding`; returns `{ text, encoding, hasBom, lossy, identity }` with the same 32 MB, plain-text and detection rules | +| `text.encode` | Encode `{ text }` like `file.write`, optional `encoding` and `bom`; returns `{ bytes, encoding, hasBom, identity }` with `bytes` in standard base64. Nothing is written, and an unmappable character fails with `encoding_unmappable` as `file.write` does | +| `document.lifecycle` | Reduce a shared document save, external-change, or conflict event without reading text or disk | +| `document.classifyText` | Classify decoded Unicode text independently of its filename | +| `document.planSaveBatch` | Select the open documents a Save All or save-before-launch batch writes | +| `document.reduceSaveBatch` | Reduce save batch member outcomes and live documents into one batch status | +| `history.record` | Store a versioned text snapshot and metadata | +| `history.entries` | List valid history entries for one file or a workspace | +| `history.content` | Read a stored history snapshot by relative storage path | +| `history.relocate` | Move retained history for a file or directory subtree after a rename or move | +| `history.planEvent` | Plan which snapshots an editing event (save, auto-save, restore, manual, discarded unsaved text, external change, before rename, before batch replace) records and under which reason | +| `history.rename` | Set or clear a user-visible label on a history entry | +| `history.delete` | Delete one history entry and its snapshot | +| `history.ignorePreview` | Validate local history ignore rules and report which rule skips a sample path | +| `history.usage` | Report one project's allocated history size and the storage cap | +| `history.clear` | Delete every snapshot of one project's history | +| `diff.compareTexts` | Compare two host-supplied texts line by line into paired side-by-side rows and unified lines | +| `execution.resolveEnvironment` | Merge native `inherited` string values and `overrides` into `{ environment }`; string overrides win. Core names no tool: a tool's runtime-home variable reaches a launch through the selected toolchain installation (`toolchains.*`), which the host applies before this merge, or as a literal override from the plugin that owns the toolchain. A structured `{ toolchain, property }` value the host could not resolve to text has no defined value and contributes nothing. `caseInsensitive: true` canonicalizes Windows keys to uppercase | +| `execution.resolveSearchPath` | Preserve existing path precedence, sort platform candidates by priority and identity, and deduplicate using the requested case policy | +| `processLifecycle.resolve` | Decide how each process or resource of one Lithe instance ends | +| `processLifecycle.inventory` | Group a process snapshot of one Lithe instance into terminals, Agent tasks and background services | +| `processLifecycle.settle` | Turn confirmation answers into what ends, what is kept and the updated settings | +| `markdown.render` | Render and sanitizes shared Markdown | +| `keymap.parseContext` | Compile a bounded boolean shortcut context | +| `keymap.evaluateContext` | Evaluate a precompiled shortcut context | +| `keymap.analyzeConflicts` | Find conflicts among the final layered shortcut bindings | +| `keymap.modifierGestureStep` | Advance the double-Shift gesture recognizer by one event. The rule itself lives in `lithe-core-rules` so the Windows frontend can run it synchronously | +| `editor.workbenchState.reduce` | Apply one editor-group state transition: open, close, move, pin, preview, merge or normalize. The rule lives in `lithe-core-rules` so the Windows frontend can run it synchronously inside store updates | +| `workspace.classifyVisibility` | Classify a batch of workspace-relative paths as hidden or visible. The rule lives in `lithe-core-rules` so the Windows frontend can run it synchronously | +| `workspace.visibilityDefaults` | Return the built-in hidden-path lists (from `lithe-core-rules`) | +| `workbench.chromeLayout.reduce` | Apply one transition to the rearrangeable workbench chrome: stripe, title bar and status bar button placement and open tool panels (from `lithe-core-rules`) | +| `terminal.parseLinkTarget` | Resolve terminal link candidates into file or external targets | +| `debug.createSession` | Create a transport-neutral DAP session and return its initialize frame | +| `debug.launch` | Queue a launch or attach request, including during initialization | +| `debug.steppingFilters` | Return adapter defaults or normalize portable stepping filters | +| `debug.relocateBreakpoints` | Move source breakpoints across one exact UTF-16 editor replacement | +| `debug.planVariablePage` | Plan the first bounded `variables` page of one scope or structured variable from its reported counts | +| `debug.reduceVariablePage` | Merge one returned variable page into its cursor and decide whether the container is exhausted | +| `debug.planDisassemblyWindow` | Plan the first `disassemble` request of the window around the instruction pointer or one listed edge | +| `debug.reduceDisassemblyPage` | Reduce one `disassemble` reply to the next bounded recovery request, a deduplicated page, or the adapter's failure | +| `debug.setBreakpoints` | Replace and deterministically order one source's DAP breakpoints | +| `debug.setExceptionBreakpoints` | Replace and deterministically order one session's exception filters | +| `debug.setFunctionBreakpoints` | Replace and deterministically order one session's named function breakpoints | +| `debug.setInstructionBreakpoints` | Replace and deterministically order one session's machine-instruction breakpoints | +| `debug.dataBreakpointInfo` | Resolve an adapter-owned data breakpoint identity for a paused variable or field | +| `debug.setDataBreakpoints` | Replace and deterministically order one session's resolved data breakpoints | +| `debug.setBreakpointsMuted` | Mute or unmutes every breakpoint of one debug session | +| `debug.planMemoryWindow` | Place the memory view's readMemory window at an address | +| `debug.locateMemoryWindow` | Find the byte read at in one readMemory reply | +| `debug.setVariable` | Replace one visible variable value in its adapter-owned parent container | +| `debug.cancelOperation` | Cancel or time out one pending operation and ignore its late response | +| `debug.execute` | Submit continue, pause, next, step-in, or step-out control | +| `debug.inspect` | Request normalized threads, frames, scopes, variables, or evaluation | +| `debug.receive` | Reduce base64-encoded bytes received from a platform-owned DAP transport | +| `debug.runInTerminalResponse` | Complete one adapter-requested native terminal launch | +| `debug.startDebuggingResponse` | Complete one adapter-requested child session start | +| `debug.disconnect` | Begin the DAP disconnect handshake without closing the native transport | +| `debug.destroySession` | Remove a session and its whole child tree after the platform closes its native transport; the response lists `destroyedChildSessionIds` (leaves first) whose transports the platform must also close | +| `lsp.applyTextEdits` | Apply LSP UTF-16 text edits with range validation | +| `lsp.planWorkspaceEdit` | Validate language-server workspace edit targets against the workspace and open documents, and order the commit | +| `lsp.documentIdentity` | Resolve a diagnostic URI or path strictly inside its workspace | +| `lsp.classifyNavigationTarget` | Classify a language-server jump target as workspace, external read-only or rejected | +| `lsp.declarationOrUsages` | Decide whether a go-to-declaration gesture opens the declaration or shows its usages | +| `editor.lineEdit` | Apply a deterministic line-level text transform and return the replacement plus selection to restore | +| `editor.find` | Find text or plans bounded replacements in a borrowed document | +| `editor.preferences.resolve` | Resolve persisted editor presentation preferences | +| `editor.recent.reduce` | Reduce and queries bounded recent-file history | +| `editor.recovery.decide` | Decide how a native checkpoint should recover | +| `keymap.schemes.reduce` | Reduce named shortcut schemes | +| `editor.planTextEdits` | Validate and order a plugin document edit batch into UTF-16 offsets with the document's line ending | +| `editor.parseNavigationInput` | Parse a bounded line or line:column input into zero-based editor coordinates | +| `editor.navigationHistory.reduce` | Reduce one bounded back/forward navigation history event and return the next state plus the location to open | +| `editor.lineCommentToken` | Resolve the line comment token for a file extension or language id | +| `network.proxyPolicy` | Normalize the stored proxy settings into the policy a network stack must apply (mode, server, credentials, exclusions) | +| `lsp.plainSnippet` | Convert LSP snippet insert text into plain editor text | +| `lsp.builtinCompletions` | Return lightweight current-file identifier completions | +| `lsp.builtinHover` | Return lightweight current-symbol hover text | +| `lsp.builtinNavigation` | Return lightweight current-file definition/reference locations | +| `lsp.mergeCompletions` | Choose which candidates survive when several prioritized providers complete at one position | +| `lsp.activationPolicy` | Decide whether a language server may start automatically after failures, using a platform monotonic clock | +| `lsp.problems.query` | Filter, order, and group Problems tool-window diagnostics and count them by severity | +| `lsp.startServer` | Start one Rust-owned process/session and begin initialization | +| `lsp.stopServer` | Gracefully shut down a session, with a bounded force-stop fallback | +| `lsp.updateWorkspaceConfiguration` | Replace the plugin configuration a running plugin-owned session answers `workspace/configuration` with, and announce it | +| `lsp.documentUri` | Convert a native absolute path into a canonical file URI | +| `lsp.syncDocument` | Open a document or apply a full-text or incremental `didChange` with monotonic versions | +| `lsp.saveDocument` | Report a confirmed disk save with its exact saved text, honoring server save options | +| `lsp.workspaceFilesChanged` | Publish normalized created, changed, or deleted workspace files to one session | +| `lsp.closeDocument` | Close a document and clear its diagnostics | +| `lsp.request` | Submit a typed semantic request and return an opaque operation ID | +| `lsp.buildRequest` | Forward one planned `lithe/build/*` request to a session that negotiated the build-provider protocol | +| `lsp.vcsRequest` | Send one VCS operation, planned by Core, to a session that negotiated the VCS-provider protocol | +| `lsp.toolchainRequest` | Send one `toolchains/query` to a plugin backend that negotiated the provider | +| `lsp.keymapImportRequest` | Send one shortcut import request to a negotiated plugin provider | +| `lsp.functionRequest` | Invoke one registered plugin function on a session its plugin started | +| `lsp.functionProvider` | Select a registered function provider strictly inside one workspace | +| `lsp.configureFunctionRelay` | Replace the enabled plugins and live workspace contexts Core's plugin-to-plugin function relay checks calls against | +| `lsp.hostResponse` | Answer one host-service request a plugin backend sent | +| `lsp.hostNotify` | Send one host notification to a plugin backend that negotiated it | +| `lsp.pluginServerRequest` | Send one request of a plugin backend to the plugin's own language server, under the plugin's declared session policy | +| `lsp.pluginServerNotify` | Send one notification of a plugin backend to the plugin's own language server | +| `lsp.cancelOperation` | Cancel one pending semantic operation | +| `lsp.pollEvents` | Drain ordered typed lifecycle/feature/diagnostic/result/log events | +| `lsp.setDetailedDiagnostics` | Toggle upstream verbose trace for one session | +| `lsp.waitEvents` | Block until queued events exist or a timeout elapses, then drain them | +| `lsp.destroyServer` | Remove a terminal session handle from the registry | +| `runConfig.inspect` | Inspect `.lithe` run documents, versions, and staleness without writing files | +| `runConfig.projectToolchain` | Read saved project tool paths for one Provider root, with the nearest path override | +| `runConfig.generate` | Regenerate detected run configurations | +| `runConfig.argumentText` | Format a batch of exact argument vectors as reversible editor text | +| `runConfig.resolve` | Merge generated, project, and local layers and return diagnostics | +| `runConfig.selectContextConfiguration` | Choose a matching effective configuration for a provider-supplied source and entry identity | +| `runConfig.decideTitleAction` | Decide what a press on the title bar's Run or Debug button does | +| `runConfig.updateOptions` | Apply typed option edits and return an updated project or local document | +| `runConfig.updateToolPreferences` | Replace only generic preferences in a captured local Run document, preserving legacy defaults and unrelated metadata | +| `runConfig.saveEditorChanges` | Prepare the local and optional project documents for one editor save | +| `runConfig.createUserConfiguration` | Validate a typed user configuration and return an updated document | +| `runConfig.createLaunchPlan` | Project one effective configuration into a platform-neutral Run or Debug plan | +| `runConfig.providerLaunchInputs` | Resolve user choices for a provider-owned launch | +| `git.status` | Resolve the repository, current branch, and working-tree changes | +| `git.untrackedDirectory` | List one level of a summarized untracked directory: its files and subdirectory summaries | +| `git.commitState` | Read exact HEAD, symbolic branch and index preconditions for a workspace commit | +| `git.workspaceCommitPrepare` | Build a reviewed multi-repository plan | +| `git.workspaceCommitStep` | Execute one guarded workspace commit/push | +| `git.classifyMetadataChanges` | Classify a bounded page of coalesced watcher paths without filesystem access | +| `git.watchContext` | Resolve the repository and absolute Git metadata roots needed by native file watchers | +| `git.worktrees` | Return deterministic registered-worktree metadata without scanning each checkout | +| `git.pullRequestContext` | Resolve worktree-aware PR branch defaults, publication state, and uncommitted-change state | +| `git.command` | Execute one argument-based Git operation and return its arguments, streams, exit code, and ordered subprocess invocations | +| `git.repositoryRoot` | Resolve the repository root containing a workspace path without acquiring repository write coordination | +| `git.resolveFileContext` | Resolve a file to its owning Git checkout and relative path | +| `git.workspaceStatusMerge` | Plan publication of fresh, retained, and unavailable repository snapshots | +| `git.write` | Validate and execute shared Git mutations such as stage, commit, branch, checkout, remote sync, clone, and stash | +| `git.rollbackPlan` | Plan whole-file rollback from explicit Git status identities without reading or changing a repository | +| `git.rollbackPrepare` | Recheck selected identities against the repository and return rollback steps | +| `git.prepareCommitSelection` | Prepare and verify the real indexes for explicitly selected changes | +| `git.reconcileDeletedSelection` | Clear missing selected staged additions after filesystem deletion | +| `git.changelists` | Reduce local ChangeList metadata and project membership and commit scope | +| `git.shelf` | Capture or applies one guarded Shelf layer | +| `git.commitMessageContext` | Capture bounded AI commit evidence and recheck its Git state | +| `git.consolePresentation` | Pure IDEA-style configuration/progress folds, empty-output notices and search ranges over retained diagnostic snapshots | +| `git.remoteUrl` | Read a configured remote URL silently; a missing remote returns a null URL | +| `git.fetchPlan` | Validate Fetch choices; optionally inspect a repository to expand enabled per-remote commands | +| `git.executable` | Resolve the executable Git will run without requiring a repository | +| `git.executionInspect` | Inspect executable capabilities, configuration provenance and effective Fetch preferences | +| `git.executionConfigure` | Explicitly save or clear one allowlisted value in a selected config scope | +| `git.authRespond` | Answer or cancel a live authentication challenge once | +| `remote.probe` | Check that an SSH connection reaches a supported host | +| `remote.authRespond` | Answer or cancel a prompt relayed by `remote.probe` or `remote.connect` once | +| `remote.connect` | Open a connection, installing the shipped server when needed | +| `remote.readDir` | List a directory over an open remote connection | +| `remote.disconnect` | Close an open connection and its OpenSSH process tree | +| `remote.closeAll` | Close every open connection; the host calls it at application exit | +| `remote.readFile` | Read a file over an open remote connection | +| `remote.writeFile` | Save a file over an open remote connection | +| `remote.hostRequest` | Perform a non-streaming operation on an existing workspace host | +| `remote.runProcess` | Own a remote process and relays output until it exits or is cancelled | +| `remote.captureProcess` | Capture a short remote process with explicit time and output budgets | +| `remote.runTerminal` | Own a remote PTY and relays output until it exits or is cancelled | +| `remote.runWatch` | Own a host watch and relays changes until cancellation or root removal | +| `remote.uploadFile` | Stream a native source into integrity-checked host file staging | +| `remote.hostInfo` | Return cached handshake capabilities of one connection | +| `git.historyRewritePreview` | Review undo, message edit, squash, or drop with complete messages, eligibility, and an immutable checkout expectation | +| `git.remotes` | Return configured fetch remote names and URLs | +| `git.tags` | Return tag names, tag object IDs, target IDs, dates, and annotations | +| `git.parseCheckoutMetadata` | Interpret bounded HEAD text without filesystem access | +| `git.parseGitdirPointer` | Interpret a bounded gitdir pointer without filesystem access | +| `git.historyActionCapabilities` | Read history menu hints from actual HEAD and the bounded first-parent chain | +| `git.rebasePreview` | Resolve the complete local linear range strictly after a selected unchanged base | +| `git.repositorySetup` | Inspect repository/unborn state and scoped/effective commit identity; see `git-repository-setup.md` | +| `git.initialize` | Initialize a directory outside existing repositories without staging or committing | +| `git.configureIdentity` | Save or clear one local/global `user.name` or `user.email` override | +| `git.rebaseStart` | Start a reviewed native interactive rebase with persisted messages and recovery identity | +| `git.rebaseSession` | Read the latest owned rebase session and distinguish edit/conflict pauses from completion | +| `git.rebaseControl` | Continue, skip, or abort an identified session, optionally amending an edit pause | +| `git.patchExport` | Export a lossless UTF-8 exchange patch | +| `git.patchPreview` | Review a patch with a forward applicability check | +| `git.patchApply` | Apply a patch tied to its reviewed snapshot | +| `git.diff` | Produce a structured working-tree, index, reference, commit, or stash patch | +| `git.parsePatch` | Parse a complete saved patch into Core-owned per-file documents | +| `git.apply` | Apply or check a patch in `stage`, `unstage`, `discard`, or Shelf restore mode | +| `git.planDiffBlocks` | Plan commit-review blocks for one file; Core owns every block patch | +| `git.applyDiffBlock` | Include, exclude or roll back one planned block under the write lease | +| `git.stageGutterChange` | Stage one editor gutter change with its original bytes | +| `git.history` | Return the legacy combined reference snapshot and first bounded commit page | +| `git.historyQuery` | Compare bounded commit date identities with explicit absolute time bounds | +| `git.references` | Return deterministic refs, recent local branches, ahead/behind state, and effective Git identity without scanning commit history | +| `git.historyPage` | Return one bounded commit page, author date, UTC offset, optional Unix timestamp, and an opaque continuation cursor | +| `git.historyCursorClose` | Release an unfinished incremental history cursor and its Git process | +| `git.historyLocate` | Append bounded history pages until one commit is found and report the budget, window cursor, and exhaustion | +| `git.branchNamePreview` | Clean and validate a new branch name against already loaded references without running Git | +| `git.repositoryColorAssignments` | Assign color slots to a bounded page of ordered repository identities | +| `git.updatePlan` | Route a branch Update and gate a Pull from facts the host collected: blockers, strategy default, whether to ask, and the expected checkout | +| `git.historyWindow` | Plan a history refresh window, page merge, and selection retention over commit identities | +| `git.graphProjection` | Project a loaded history window into commit-graph lanes, color IDs, routes, print elements, and labels, optionally filtered | +| `git.normalizeChangeTargets` | Drop targets already covered by a selected untracked summary | +| `git.rebasePlanPreview` | Validate an interactive rebase draft and derives its messages | +| `git.referenceTree` | Group one kind of loaded references into an ordered tree | +| `git.pushPreview` | Resolve a local branch push destination and the bounded commits not present on that remote base | +| `git.commit` | Return one structured commit by revision with its full message body | +| `git.commitFiles` | Return files changed by one commit | +| `git.comparison` | Return files changed between a reference and the working tree | +| `git.stashes` | Return structured stash references and messages | +| `git.checkoutPreflight` | Return local paths that would block switching to a reference | +| `git.pullPreflight` | Report the configured upstream, ahead/behind counts, divergence, tracked local changes, and the strategy Git is configured for, without fetching | +| `git.integrationPreflight` | Return local paths that block a merge, rebase, cherry-pick, or revert | +| `git.conflictMarkers` | Return staged text files that still contain conflict markers | +| `git.operationState` | Report an interrupted merge, rebase, cherry-pick, or revert and its conflicted paths | +| `git.blame` | Return structured line blame metadata | +| `git.lineHistory` | Trace which commits touched a one-based snapshot line range of one file | +| `github.parseRemote` | Parse a canonical GitHub HTTPS or SSH remote into owner/name | +| `github.requestPlan` | Validate one GitHub operation and produce a trusted platform HTTP request plan | +| `github.normalizeResponse` | Normalize raw GitHub JSON and HTTP status into deterministic data or a stable error | +| `github.deviceAuthorizationTransition` | Plan the next device-authorization action, polling interval, and deadline-bounded wait | +| `github.pullRequestCheckoutPlan` | Plan a PR's safe local branch and report an existing-ref conflict | +| `git.pullRequestCheckoutPreview` | Inspect and review a non-destructive PR checkout plan | +| `git.pullRequestCheckout` | Recheck and execute a reviewed PR checkout without resetting an existing branch | +| `diagnostics.redactText` | Redact credentials, tokens, and home-directory paths from diagnostic-bundle text | +| `diagnostics.prepareLogEvent` | Normalize a bounded application event and recursively redact structured fields | +| `diagnostics.queryLogs` | Merge and filter bounded host-collected module log tails | +| `diagnostics.recordPluginOperation` | Record host-observed declarative contribution outcomes | +| `diagnostics.drainCoreEvents` | Atomically take process-owned Core log facts and their dropped count | +| `diagnostics.configureCoreEvents` | Set the DEBUG admission gate for Core log facts from host detailed-diagnostics state | +| `diagnostics.encodeLogBatch` | Encode independent structured v2 log events with per-item errors | +| `diagnostics.parseLogFilter` | Parse bounded filter expressions and UTF-16 diagnostic ranges | +| `diagnostics.composeLogFilter` | Compose a filter predicate preserving unrelated terms | +| `diagnostics.formatLogRecords` | Validate and format canonical redacted records for copying | +| `diagnostics.queryLogsV2` | Query structured v2 and legacy v1 native log tails | +| `diagnostics.planLogCollection` | Plan bounded native log-tail reads from raw file metadata | +| `diagnostics.planLogRetention` | Plan log cleanup from host-owned file inventories | +| `diagnostics.resolveLogChannel` | Validate channels and resolve safe session-relative destinations | +| `diagnostics.resolveEngineLogSubject` | Resolve the same routing segment and channel used by engine startup, without I/O | +| `diagnostics.buildManifest` | Shape a deterministic diagnostic bundle manifest from host-gathered environment and file facts | + Workspace paths in responses are relative and use `/` separators. Line numbers are one-based. `git.status.repositoryRoot` may be an absolute path when the opened workspace is a subdirectory of the repository; all Git change paths are @@ -3327,6 +3709,27 @@ natively; this command only shapes and sorts them. The additive top-level `redaction:"core-v2"` identifies the shared redaction boundary; schemaVersion remains 1. Windows consumes the original fields and may ignore this marker. +### AI configuration + +`ai.configuration.sources` takes `{source, home, environment}` (`source` is +`codex` or `claude`, `home` an absolute path, `environment` only the directory +overrides `CODEX_HOME` / `CLAUDE_CONFIG_DIR`) and returns `{files}`: the native +files `{file, path}` the platform should read. `ai.configuration.parse` takes +`{source, files, environment, allowIncomplete}` with the file texts keyed +`config`, `auth`, `settings`, `credentials` and `root`, each at most 1 MiB, and +returns `null` when nothing is configured, otherwise `{model, provider, issue, +reasoningEffort, hasCredential, credential, unsupportedAuthentication}`. The +response never contains a secret: `credential` is a locator +(`{kind: "environment", name}` or `{kind: "file", file, path}`) that the host +resolves natively against the same text or environment. Malformed input returns +`invalid_request` with a fixed code (`AI_COMMIT_INVALID_CONFIG`, +`AI_CONFIGURATION_INVALID_AUTHENTICATION`, `AI_CONFIGURATION_SMART_QUOTES`, +`AI_CONFIGURATION_TOO_LARGE`) and never echoes input. `allowIncomplete: true` is +for editor drafts: it ignores the environment, accepts blank or partial text and +keeps missing endpoint/model fields empty. The shared cases are in +`rust/fixtures/ai/cli-configuration-v1.json`. It replaces the former +`agent.parseProviderConfiguration`. + ### Declarative plugins The `plugins.declarative.*` commands implement @@ -3772,6 +4175,8 @@ and the directories are absolute host paths used only as substitution text. | --- | --- | --- | | `build.detect` | `{ workspaceRoot, plugins: [{ manifest }], options?: { : { : value } }, instanceOptions?: { : { : { : value } } }, instanceDefaults?: { : { : value } }, emptySelections?: { : { : [optionID] } } }` | `detection`: `{ instances, rejected: [{ index, error }], shadowed: [{ pluginID, buildSystemID, by }] }`; each instance is `{ id, pluginID, buildSystemID, displayName, root, markers, modelSource, optionFingerprint, options, droppedOptions }` plus `model` or `modelError` for `static` and `hostIntegration` sources; a `provider` source has neither. `options` contains accepted stored values after Core scope/root merging, including explicit empty selections; `droppedOptions` names rejected stored values without rewriting the source document. | | `build.projectOptions` | `{ document }`, host-read persisted v1 Build options, without a manifest | `document`: validated storage with version 1 and systems; options, instance defaults, root values, explicit empty selections and saved custom choices are retained. Missing system fields normalize to empty maps; malformed, unsupported or oversized documents reject with `plugin.build.invalidOptionsDocument`. This pure operation never reads or writes a file and does not replace declared option resolution. | +| `build.resolveStoredOptions` | `{ document, local: { version: 1, document, emptySelections?: { : { : [optionID] } } } }`, without a manifest | `storage: { document, local, emptySelections }`: checked team/local merge; local values override matching keys, unrelated systems/roots/choices stay intact. No filesystem I/O. Local documents are machine/workspace preferences, not team files or plugin-native configuration. Their storage syntax is [build-local-options-v1.schema.json](build-local-options-v1.schema.json), with additional cross-field membership and byte limits checked by Core. | +| `build.editStoredOption` | `{ document, local, root, optionID, value, modelValues? }` in the standard manifest/build-system envelope | `edit: { local, options }`: changes one declared local preference. `null` removes the local override and restores team/declaration inheritance. Invalid or partially accepted edits reject before persistence. Workspace multi-selections preserve sibling-only values; explicit per-root empty markers apply only to workspace multi-select declarations. Platforms compare both native source revisions, atomically write only the local document and read back. | | `build.invocationPlan` | `{ root, platform: "macos" \| "linux" \| "windows" }` | `invocation`: `{ buildSystemID, root, executables, environmentInputs, runtimeDependencies, version, minimumVersion }` with project-file paths joined to `root`; every runtime dependency with a known probe, host-managed ones included, carries `version: { arguments }` | | `build.checkToolVersion` | `{ runtime?, output }` | `version`: `{ satisfied, version? }`; `runtime` checks that runtime dependency instead of the tool | | `build.resolveModelInputs` | `{revision, sources: [{manifest, instances: [{buildSystemID, root, state, model?, options?}]}]}` from installed enabled current-workspace sources | `modelInputs`: version 1 public snapshot with exact optional producer slots, state, normalized projects/targets/content roots, configuration fingerprint and explicitly shared options; no tasks/run/settings/private functions; see the [plugin contract](https://github.com/1lck/Lithe-IDEA/blob/preview/shared/plugins/contract/v1/README.md#optional-read-only-model-inputs) | diff --git a/rust/fixtures/i18n/format-v1.json b/rust/fixtures/i18n/format-v1.json deleted file mode 100644 index 26a36794..00000000 --- a/rust/fixtures/i18n/format-v1.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "schemaVersion": 1, - "parameters": { "name": "string", "count": "integer" }, - "arguments": ["name", "count"], - "cases": [ - { - "name": "Reordered parameters and literal percent", - "template": "100% {count} 个:{name}", - "nativeTemplate": "100%% %2$lld 个:%1$@", - "stringValue": "中文/%@/{count}", - "integerValue": 9000000000, - "expected": "100% 9000000000 个:中文/%@/{count}" - }, - { - "name": "Repeated parameter and zero count", - "template": "{name}: {count} files ({name})", - "nativeTemplate": "%1$@: %2$lld files (%1$@)", - "stringValue": "feature/100%", - "integerValue": 0, - "expected": "feature/100%: 0 files (feature/100%)" - } - ] -} diff --git a/rust/fixtures/i18n/native-abi-v1.json b/rust/fixtures/i18n/native-abi-v1.json deleted file mode 100644 index 75991068..00000000 --- a/rust/fixtures/i18n/native-abi-v1.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "schemaVersion": 1, - "parameters": { "rows": "unsignedInteger", "elapsed": "integer" }, - "macos": { "formats": { "rows": "llu", "elapsed": "d" } }, - "arguments": ["rows", "elapsed"], - "template": "{rows} rows affected in {elapsed} ms", - "nativeTemplate": "%1$llu rows affected in %2$d ms", - "unsignedValue": "18446744073709551615", - "integerValue": 2147483647, - "expected": "18446744073709551615 rows affected in 2147483647 ms" -} diff --git a/rust/fixtures/phpunit-project/.gitignore b/rust/fixtures/phpunit-project/.gitignore deleted file mode 100644 index c84ab0ca..00000000 --- a/rust/fixtures/phpunit-project/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/vendor/ -/composer.lock -/.phpunit.result.cache diff --git a/rust/fixtures/phpunit-project/composer.json b/rust/fixtures/phpunit-project/composer.json deleted file mode 100644 index d7667b3f..00000000 --- a/rust/fixtures/phpunit-project/composer.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "lithe/phpunit-fixture", - "require-dev": { "phpunit/phpunit": "9.6.22" }, - "scripts": { "test": ["@php vendor/bin/phpunit"] } -} diff --git a/rust/fixtures/phpunit-project/phpunit.xml b/rust/fixtures/phpunit-project/phpunit.xml deleted file mode 100644 index ddf7f01b..00000000 --- a/rust/fixtures/phpunit-project/phpunit.xml +++ /dev/null @@ -1 +0,0 @@ -tests diff --git a/rust/fixtures/phpunit-project/src/Store.php b/rust/fixtures/phpunit-project/src/Store.php deleted file mode 100644 index 1f61a213..00000000 --- a/rust/fixtures/phpunit-project/src/Store.php +++ /dev/null @@ -1,6 +0,0 @@ -values[] = $value; } - public function values(): array { return $this->values; } -} diff --git a/rust/fixtures/phpunit-project/tests/StoreTest.php b/rust/fixtures/phpunit-project/tests/StoreTest.php deleted file mode 100644 index 63a5e973..00000000 --- a/rust/fixtures/phpunit-project/tests/StoreTest.php +++ /dev/null @@ -1,12 +0,0 @@ -add('a'); $store->add('b'); - self::assertSame(['a', 'b'], $store->values()); - } - public function testKeepsInsertionOrderWithDuplicates(): void { - $store = new Store(); $store->add('a'); $store->add('a'); - self::assertSame(['a', 'a'], $store->values()); - } -} diff --git a/rust/lithe-core/src/protocol/command.rs b/rust/lithe-core/src/protocol/command.rs index d261453f..02619e61 100644 --- a/rust/lithe-core/src/protocol/command.rs +++ b/rust/lithe-core/src/protocol/command.rs @@ -1223,6 +1223,78 @@ impl CoreCommand { #[cfg(test)] mod tests { use super::CoreCommand; + use std::collections::BTreeSet; + use std::path::Path; + + /// Returns the command names that `CoreCommand::parse` matches, read from + /// this file so the test cannot drift from the parser it checks. + fn parsed_command_names(source: &str) -> Vec { + let body = source + .split_once("pub fn parse(value: &str) -> Option {") + .and_then(|(_, rest)| rest.split_once("_ => None,")) + .map(|(body, _)| body) + .expect("CoreCommand::parse body"); + body.lines() + .filter_map(|line| line.trim().strip_prefix('"')) + .filter_map(|line| line.split_once("\" =>")) + .map(|(name, _)| name.to_owned()) + .collect() + } + + /// Returns the first-column names of the `| Command | Purpose |` table. + fn documented_command_names(document: &str) -> Vec { + document + .lines() + .skip_while(|line| !line.starts_with("| Command | Purpose |")) + .skip(2) + .take_while(|line| line.starts_with("| `")) + .map(|line| { + line["| `".len()..] + .split_once('`') + .expect("backticked command name") + .0 + .to_owned() + }) + .collect() + } + + #[test] + fn api_command_table_lists_exactly_the_parsed_commands() { + let crate_root = Path::new(env!("CARGO_MANIFEST_DIR")); + let source = std::fs::read_to_string(crate_root.join("src/protocol/command.rs")) + .expect("read command.rs"); + let document = std::fs::read_to_string(crate_root.join("../contracts/rust-core-api.md")) + .expect("read rust-core-api.md"); + let parsed = parsed_command_names(&source); + let documented = documented_command_names(&document); + + assert!( + parsed.len() > 300, + "parser scan found {} names", + parsed.len() + ); + for name in &parsed { + assert!( + CoreCommand::parse(name).is_some(), + "scanned {name} does not parse" + ); + } + let unique: BTreeSet<_> = documented.iter().collect(); + assert_eq!( + unique.len(), + documented.len(), + "duplicate rows in the command table" + ); + + let parsed: BTreeSet<_> = parsed.iter().collect(); + let undocumented: Vec<_> = parsed.difference(&unique).collect(); + let unknown: Vec<_> = unique.difference(&parsed).collect(); + assert!( + undocumented.is_empty() && unknown.is_empty(), + "rust-core-api.md command table is out of date: missing {undocumented:?}, \ + not parsed {unknown:?}" + ); + } #[test] fn parses_semantic_lsp_runtime_commands() { From 9cc81d6948110d8cc54bf3641558d69ebab368a3 Mon Sep 17 00:00:00 2001 From: Yao Jingxi <23722032@bjtu.edu.cn> Date: Sat, 14 Nov 2026 00:21:41 +0800 Subject: [PATCH 5/5] =?UTF-8?q?refactor(ai):=20AI=20=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E6=8B=86=E6=88=90=E7=8B=AC=E7=AB=8B=20crate=20lithe-ai?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rust/lithe-core/src/ai 整体移到 rust/lithe-ai:提交信息生成、PR 描述、内联编辑、 AI CLI 配置解析。新 crate 只依赖 serde、serde_json、toml、url,不做文件、网络和凭据访问。 - lithe-ai 提供 COMMANDS / execute / execute_json 和 AiError,形状与 lithe-core-rules 一致; lithe-core 的 9 个 ai.* 分发分支改为调用 lithe_ai::execute,AiError 一一映射到 CoreError。 - git.commitMessageContext 仍在 lithe-core(要跑 git),改用 lithe_ai 的敏感路径与预算规则。 - lithe_core::ai 改为 re-export lithe_ai,原有路径不变。命令名、C ABI、JSON 和错误码不变。 - 注释规范检查覆盖 lithe-ai;新增测试确认每个 lithe-ai 命令都经 Core 路由到 lithe-ai。 --- rust/Cargo.lock | 11 +++ rust/Cargo.toml | 2 +- rust/contracts/ai-commit.md | 3 + rust/contracts/ai-generation-requests.md | 2 + rust/contracts/rust-core-api.md | 3 + rust/lithe-ai/Cargo.toml | 14 +++ .../src}/agent_configuration.rs | 12 +-- .../src/ai => lithe-ai/src}/commands.rs | 26 +++--- .../src/ai => lithe-ai/src}/configuration.rs | 0 rust/lithe-ai/src/dispatch.rs | 89 +++++++++++++++++++ rust/lithe-ai/src/error.rs | 37 ++++++++ .../src/ai => lithe-ai/src}/generation.rs | 21 +++-- .../src/ai => lithe-ai/src}/inline_edit.rs | 10 +-- rust/lithe-ai/src/lib.rs | 48 ++++++++++ .../ai => lithe-ai/src}/provider_request.rs | 0 .../src/ai => lithe-ai/src}/pull_request.rs | 0 .../src/ai => lithe-ai/src}/request_tests.rs | 4 +- .../src/ai => lithe-ai/src}/tests.rs | 14 ++- rust/lithe-core/Cargo.toml | 1 + rust/lithe-core/src/ai/mod.rs | 39 -------- .../src/git/commit_message_context.rs | 10 +-- rust/lithe-core/src/lib.rs | 5 +- rust/lithe-core/src/protocol/error.rs | 11 +++ rust/lithe-core/src/runtime/dispatcher.rs | 36 ++++---- rust/lithe-core/src/tests/protocol.rs | 31 +++++++ scripts/rust-core/README.md | 2 +- .../rust-core/verify-rust-core-comments.sh | 7 +- 27 files changed, 328 insertions(+), 110 deletions(-) create mode 100644 rust/lithe-ai/Cargo.toml rename rust/{lithe-core/src/ai => lithe-ai/src}/agent_configuration.rs (96%) rename rust/{lithe-core/src/ai => lithe-ai/src}/commands.rs (84%) rename rust/{lithe-core/src/ai => lithe-ai/src}/configuration.rs (100%) create mode 100644 rust/lithe-ai/src/dispatch.rs create mode 100644 rust/lithe-ai/src/error.rs rename rust/{lithe-core/src/ai => lithe-ai/src}/generation.rs (93%) rename rust/{lithe-core/src/ai => lithe-ai/src}/inline_edit.rs (97%) create mode 100644 rust/lithe-ai/src/lib.rs rename rust/{lithe-core/src/ai => lithe-ai/src}/provider_request.rs (100%) rename rust/{lithe-core/src/ai => lithe-ai/src}/pull_request.rs (100%) rename rust/{lithe-core/src/ai => lithe-ai/src}/request_tests.rs (98%) rename rust/{lithe-core/src/ai => lithe-ai/src}/tests.rs (98%) delete mode 100644 rust/lithe-core/src/ai/mod.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index a69e7b78..68289797 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -950,6 +950,16 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "lithe-ai" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "toml", + "url", +] + [[package]] name = "lithe-core" version = "0.1.0" @@ -960,6 +970,7 @@ dependencies = [ "comrak", "encoding_rs", "ignore", + "lithe-ai", "lithe-core-rules", "lithe-git-host", "lithe-ide-host", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 092d56cb..a5b0825b 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["lithe-core", "lithe-core-rules", "lithe-git-host", "lithe-ide-host", "lithe-remote"] +members = ["lithe-ai", "lithe-core", "lithe-core-rules", "lithe-git-host", "lithe-ide-host", "lithe-remote"] resolver = "2" [profile.release] diff --git a/rust/contracts/ai-commit.md b/rust/contracts/ai-commit.md index addf0c4d..8dfd1f51 100644 --- a/rust/contracts/ai-commit.md +++ b/rust/contracts/ai-commit.md @@ -2,6 +2,9 @@ Version: 1. Behavioral fixture: `rust/fixtures/ai/commit-generation-v1.json`. +Every `ai.*` command is implemented in the `lithe-ai` crate (`rust/lithe-ai`), which +`lithe-core` routes to and re-exports as `lithe_core::ai`. + Windows consumes the typed Rust API in `lithe_core::ai`; macOS uses the same planner and decoder through the Core JSON commands `ai.planCommit` and `ai.decodeCommitMessage`. The API has no filesystem, HTTP, operating-system diff --git a/rust/contracts/ai-generation-requests.md b/rust/contracts/ai-generation-requests.md index de982558..de1bf7df 100644 --- a/rust/contracts/ai-generation-requests.md +++ b/rust/contracts/ai-generation-requests.md @@ -3,6 +3,8 @@ Version: 1. Behavioral fixtures: `rust/fixtures/ai/pull-request-description-v1.json` and `rust/fixtures/ai/inline-edit-v1.json`. +Both are implemented in the `lithe-ai` crate (`rust/lithe-ai`), like every `ai.*` command. + Both features reuse the credential-free provider request model of [AI commit generation](ai-commit.md): Core validates the endpoint, appends the protocol path exactly once, encodes a non-streaming body, and extracts text from diff --git a/rust/contracts/rust-core-api.md b/rust/contracts/rust-core-api.md index 0d6cd826..b0e40751 100644 --- a/rust/contracts/rust-core-api.md +++ b/rust/contracts/rust-core-api.md @@ -3711,6 +3711,9 @@ Windows consumes the original fields and may ignore this marker. ### AI configuration +These commands, like every `ai.*` command, are implemented in the `lithe-ai` crate +(`rust/lithe-ai`); `lithe-core` only routes them. + `ai.configuration.sources` takes `{source, home, environment}` (`source` is `codex` or `claude`, `home` an absolute path, `environment` only the directory overrides `CODEX_HOME` / `CLAUDE_CONFIG_DIR`) and returns `{files}`: the native diff --git a/rust/lithe-ai/Cargo.toml b/rust/lithe-ai/Cargo.toml new file mode 100644 index 00000000..b8058915 --- /dev/null +++ b/rust/lithe-ai/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "lithe-ai" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +publish = false +description = "Credential-free AI request planning and response decoding for Lithe Core" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +# preserve_order matches lithe-core: planned request bodies keep their field order. +serde_json = { version = "1.0", features = ["preserve_order"] } +toml = { version = "1.1.4", default-features = false, features = ["parse", "serde"] } +url = "2.5" diff --git a/rust/lithe-core/src/ai/agent_configuration.rs b/rust/lithe-ai/src/agent_configuration.rs similarity index 96% rename from rust/lithe-core/src/ai/agent_configuration.rs rename to rust/lithe-ai/src/agent_configuration.rs index d8642986..8df0dfab 100644 --- a/rust/lithe-core/src/ai/agent_configuration.rs +++ b/rust/lithe-ai/src/agent_configuration.rs @@ -9,7 +9,7 @@ use super::configuration::{ self, codex_model, configuration_sources, ConfigurationFiles, DetectedConfiguration, INVALID_AUTHENTICATION, INVALID_CONFIG, }; -use crate::protocol::{CoreError, ErrorCode}; +use crate::{AiError, AiErrorCode}; use serde::Deserialize; use serde_json::{json, Value}; use std::collections::BTreeMap; @@ -24,11 +24,11 @@ const MAXIMUM_ENVIRONMENT_VALUE_BYTES: usize = 32 * 1024; const TOO_LARGE: &str = "AI_CONFIGURATION_TOO_LARGE"; /// Fixed messages only: parser diagnostics may quote a pasted credential. -fn invalid(code: &'static str) -> CoreError { - CoreError::new(ErrorCode::InvalidRequest, code) +fn invalid(code: &'static str) -> AiError { + AiError::new(AiErrorCode::InvalidRequest, code) } -fn bounded_environment(environment: &BTreeMap) -> Result<(), CoreError> { +fn bounded_environment(environment: &BTreeMap) -> Result<(), AiError> { if environment.len() > MAXIMUM_ENVIRONMENT_ENTRIES || environment .iter() @@ -40,7 +40,7 @@ fn bounded_environment(environment: &BTreeMap) -> Result<(), Cor } /// Implements `ai.configuration.sources`: the native files of one source. -pub fn configuration_sources_command(payload: Value) -> Result { +pub fn configuration_sources_command(payload: Value) -> Result { #[derive(Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct Request { @@ -68,7 +68,7 @@ pub fn configuration_sources_command(payload: Value) -> Result /// with `issue` when the configuration parses but is not a usable provider. /// Draft mode (`allowIncomplete`) parses editor text only: it ignores the /// environment and keeps missing endpoint and model fields empty. -pub fn parse_configuration_command(payload: Value) -> Result { +pub fn parse_configuration_command(payload: Value) -> Result { #[derive(Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct Request { diff --git a/rust/lithe-core/src/ai/commands.rs b/rust/lithe-ai/src/commands.rs similarity index 84% rename from rust/lithe-core/src/ai/commands.rs rename to rust/lithe-ai/src/commands.rs index 9784c8ce..5b59a31f 100644 --- a/rust/lithe-core/src/ai/commands.rs +++ b/rust/lithe-ai/src/commands.rs @@ -5,7 +5,7 @@ use super::{ decode_message, decode_pull_request, plan_commit_bounded, plan_pull_request, CommitFile, CommitOptions, Provider, PullRequestComparison, PullRequestOptions, }; -use crate::protocol::{CoreError, ErrorCode}; +use crate::{AiError, AiErrorCode}; use serde::Deserialize; use serde_json::{json, Value}; @@ -32,11 +32,11 @@ struct DecodeRequest { } // Avoid echoing untrusted payloads, including diffs or provider responses, in diagnostics. -fn invalid_payload() -> CoreError { - CoreError::new(ErrorCode::InvalidRequest, "AI_COMMIT_INVALID_OPTIONS") +fn invalid_payload() -> AiError { + AiError::new(AiErrorCode::InvalidRequest, "AI_COMMIT_INVALID_OPTIONS") } -pub(crate) fn plan_commit_command(payload: Value) -> Result { +pub(crate) fn plan_commit_command(payload: Value) -> Result { let request: PlanRequest = serde_json::from_value(payload).map_err(|_| invalid_payload())?; let lengths: Vec = request .files @@ -46,21 +46,21 @@ pub(crate) fn plan_commit_command(payload: Value) -> Result { .ok_or_else(invalid_payload)?; let files = request.files; let plan = plan_commit_bounded(&request.provider, &request.options, &files, &lengths) - .map_err(|code| CoreError::new(ErrorCode::InvalidRequest, code))?; + .map_err(|code| AiError::new(AiErrorCode::InvalidRequest, code))?; Ok(json!({"url": plan.url, "body": plan.body})) } -pub(crate) fn decode_commit_command(payload: Value) -> Result { +pub(crate) fn decode_commit_command(payload: Value) -> Result { let request: DecodeRequest = serde_json::from_value(payload).map_err(|_| invalid_payload())?; let message = decode_message(&request.protocol, &request.response, request.include_body) - .map_err(|code| CoreError::new(ErrorCode::InvalidRequest, code))?; + .map_err(|code| AiError::new(AiErrorCode::InvalidRequest, code))?; Ok(json!({"message": message})) } -pub(crate) fn commit_input_budget_command(payload: Value) -> Result { +pub(crate) fn commit_input_budget_command(payload: Value) -> Result { let request: BudgetRequest = serde_json::from_value(payload).map_err(|_| invalid_payload())?; let files = commit_input_budget(request.maximum_diff_characters, &request.file_lengths) - .map_err(|code| CoreError::new(ErrorCode::InvalidRequest, code))?; + .map_err(|code| AiError::new(AiErrorCode::InvalidRequest, code))?; Ok(json!({"files": files})) } @@ -78,19 +78,19 @@ struct PullRequestDecodeRequest { } /// `ai.pullRequest.plan`: credential-free request for a pull request title and description. -pub(crate) fn plan_pull_request_command(payload: Value) -> Result { +pub(crate) fn plan_pull_request_command(payload: Value) -> Result { let request: PullRequestPlanRequest = serde_json::from_value(payload).map_err(|_| invalid_payload())?; let plan = plan_pull_request(&request.provider, &request.options, &request.comparison) - .map_err(|code| CoreError::new(ErrorCode::InvalidRequest, code))?; + .map_err(|code| AiError::new(AiErrorCode::InvalidRequest, code))?; Ok(json!({"url": plan.url, "body": plan.body})) } /// `ai.pullRequest.decode`: strict title and description from a provider response. -pub(crate) fn decode_pull_request_command(payload: Value) -> Result { +pub(crate) fn decode_pull_request_command(payload: Value) -> Result { let request: PullRequestDecodeRequest = serde_json::from_value(payload).map_err(|_| invalid_payload())?; let description = decode_pull_request(&request.protocol, &request.response) - .map_err(|code| CoreError::new(ErrorCode::InvalidRequest, code))?; + .map_err(|code| AiError::new(AiErrorCode::InvalidRequest, code))?; serde_json::to_value(description).map_err(|_| invalid_payload()) } diff --git a/rust/lithe-core/src/ai/configuration.rs b/rust/lithe-ai/src/configuration.rs similarity index 100% rename from rust/lithe-core/src/ai/configuration.rs rename to rust/lithe-ai/src/configuration.rs diff --git a/rust/lithe-ai/src/dispatch.rs b/rust/lithe-ai/src/dispatch.rs new file mode 100644 index 00000000..2589512f --- /dev/null +++ b/rust/lithe-ai/src/dispatch.rs @@ -0,0 +1,89 @@ +//! Command routing and the JSON envelope shared with `lithe-core`. + +use crate::{agent_configuration, commands, inline_edit, AiError, AiErrorCode}; +use serde::Deserialize; +use serde_json::{json, Value}; + +/// Every command name this crate owns. `lithe-core` routes exactly these names here. +pub const COMMANDS: &[&str] = &[ + "ai.commitInputBudget", + "ai.planCommit", + "ai.decodeCommitMessage", + "ai.configuration.sources", + "ai.configuration.parse", + "ai.pullRequest.plan", + "ai.pullRequest.decode", + "ai.inlineEdit.plan", + "ai.inlineEdit.decode", +]; + +/// Reports whether `command` is owned by this crate. +pub fn is_ai_command(command: &str) -> bool { + COMMANDS.contains(&command) +} + +/// Runs one AI command on an already decoded payload. +pub fn execute(command: &str, payload: Value) -> Result { + match command { + "ai.commitInputBudget" => commands::commit_input_budget_command(payload), + "ai.planCommit" => commands::plan_commit_command(payload), + "ai.decodeCommitMessage" => commands::decode_commit_command(payload), + "ai.configuration.sources" => agent_configuration::configuration_sources_command(payload), + "ai.configuration.parse" => agent_configuration::parse_configuration_command(payload), + "ai.pullRequest.plan" => commands::plan_pull_request_command(payload), + "ai.pullRequest.decode" => commands::decode_pull_request_command(payload), + "ai.inlineEdit.plan" => inline_edit::plan_command(payload), + "ai.inlineEdit.decode" => inline_edit::decode_command(payload), + _ => Err(AiError::new( + AiErrorCode::NotSupported, + "Unknown AI command", + )), + } +} + +#[derive(Deserialize)] +struct Request { + id: Option, + command: String, + #[serde(default)] + payload: Value, +} + +/// Executes one Core-style JSON request and returns the JSON response envelope. +/// +/// Unknown envelope fields (`operationId`, `origin`, ...) are ignored so callers can send the +/// same request object they send to `lithe-core`. +pub fn execute_json(request: &str) -> String { + let parsed: Request = match serde_json::from_str(request) { + Ok(parsed) => parsed, + Err(_) => { + let error = AiError::new(AiErrorCode::InvalidRequest, "Invalid JSON request"); + return failure(None, &error); + } + }; + match execute(&parsed.command, parsed.payload) { + Ok(data) => json!({"id": parsed.id, "ok": true, "data": data}).to_string(), + Err(error) => failure(parsed.id, &error), + } +} + +fn failure(id: Option, error: &AiError) -> String { + json!({"id": id, "ok": false, "error": error}).to_string() +} + +#[cfg(test)] +mod tests { + use super::{execute, COMMANDS}; + use crate::AiErrorCode; + use serde_json::json; + + #[test] + fn every_listed_command_is_routed() { + for command in COMMANDS { + let error = execute(command, json!(null)).expect_err(command); + assert_eq!(error.code, AiErrorCode::InvalidRequest, "{command}"); + } + let unknown = execute("ai.unknown", json!({})).unwrap_err(); + assert_eq!(unknown.code, AiErrorCode::NotSupported); + } +} diff --git a/rust/lithe-ai/src/error.rs b/rust/lithe-ai/src/error.rs new file mode 100644 index 00000000..7d41264d --- /dev/null +++ b/rust/lithe-ai/src/error.rs @@ -0,0 +1,37 @@ +//! Failure type whose JSON shape matches the Core error envelope. + +use serde::Serialize; + +/// Stable failure categories. Names serialize exactly like the matching `lithe-core` codes. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum AiErrorCode { + /// The payload does not match the command contract, or the planner refused it. The + /// message is a stable `AI_*` code that platforms map to their own wording. + InvalidRequest, + /// The command name is not owned by this crate. + NotSupported, +} + +/// An AI planning failure with a stable category and a message that is safe to display. +/// +/// Messages are fixed codes: diffs, pasted configuration and provider responses can carry +/// credentials or private source, so no failure echoes its input. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AiError { + /// Machine-readable category. + pub code: AiErrorCode, + /// Stable failure code such as `AI_COMMIT_INVALID_OPTIONS`. + pub message: String, +} + +impl AiError { + /// Creates an error from a category and a stable failure code. + pub fn new(code: AiErrorCode, message: impl Into) -> Self { + Self { + code, + message: message.into(), + } + } +} diff --git a/rust/lithe-core/src/ai/generation.rs b/rust/lithe-ai/src/generation.rs similarity index 93% rename from rust/lithe-core/src/ai/generation.rs rename to rust/lithe-ai/src/generation.rs index 9ee6bbed..b50885a4 100644 --- a/rust/lithe-core/src/ai/generation.rs +++ b/rust/lithe-ai/src/generation.rs @@ -133,10 +133,13 @@ pub(crate) fn plan_commit_bounded( plan_commit_with_truncation(provider, options, files, &truncated) } +/// The diff prefix one file may send, in characters, and whether that cuts the diff. #[derive(Serialize)] -pub(crate) struct InputBudget { - pub(crate) characters: usize, - pub(crate) truncated: bool, +pub struct InputBudget { + /// Characters of the file's diff that fit in its share of the total budget. + pub characters: usize, + /// Whether the prefix is shorter than the whole diff. + pub truncated: bool, } // The JSON boundary and the typed planner use the same allocation state. @@ -163,8 +166,12 @@ impl DiffBudget { } } -// Keep large editor-owned diffs on the host and pass only Core-selected prefixes. -pub(crate) fn commit_input_budget( +/// Splits `maximum` diff characters across files of the given `lengths`. +/// +/// Keeps large editor-owned diffs on the host: only the Core-selected prefixes cross the +/// boundary. Fails with `AI_COMMIT_INVALID_OPTIONS` outside 8,000–120,000 characters or above +/// 1,000 files. +pub fn commit_input_budget( maximum: usize, lengths: &[usize], ) -> Result, &'static str> { @@ -310,7 +317,9 @@ pub(crate) fn chat_token_field(field: &ChatTokenLimitField) -> ChatTokenField { } } -pub(crate) fn sensitive_path(path: &str) -> bool { +/// Reports whether `path` names a file that commonly holds credentials (`.env`, private keys, +/// `auth.json` and similar). Commit generation refuses to send such files to a provider. +pub fn sensitive_path(path: &str) -> bool { let path = path.replace('\\', "/").to_lowercase(); let name = path.rsplit('/').next().unwrap_or(""); name == ".env" diff --git a/rust/lithe-core/src/ai/inline_edit.rs b/rust/lithe-ai/src/inline_edit.rs similarity index 97% rename from rust/lithe-core/src/ai/inline_edit.rs rename to rust/lithe-ai/src/inline_edit.rs index c4d86fde..98bd0d56 100644 --- a/rust/lithe-core/src/ai/inline_edit.rs +++ b/rust/lithe-ai/src/inline_edit.rs @@ -9,7 +9,7 @@ use super::provider_request::{ encode_body, endpoint_url, response_text, strip_fence, ChatTokenField, EndpointError, PromptRequest, ResponseError, WireProtocol, }; -use crate::protocol::{CoreError, ErrorCode}; +use crate::{AiError, AiErrorCode}; use serde::Deserialize; use serde_json::{json, Value}; @@ -106,13 +106,13 @@ struct DecodeRequest { response: Value, } -fn failure(code: &'static str) -> CoreError { +fn failure(code: &'static str) -> AiError { // Codes only: edit text and provider responses never enter diagnostics. - CoreError::new(ErrorCode::InvalidRequest, code) + AiError::new(AiErrorCode::InvalidRequest, code) } /// `ai.inlineEdit.plan`: returns the URL, body, and credential placement for one edit request. -pub(crate) fn plan_command(payload: Value) -> Result { +pub(crate) fn plan_command(payload: Value) -> Result { let request: PlanRequest = serde_json::from_value(payload).map_err(|_| failure("AI_INLINE_EDIT_INVALID_REQUEST"))?; plan(request).map_err(failure) @@ -257,7 +257,7 @@ fn tail(text: &str, characters: usize) -> &str { /// /// An empty replacement is a valid result the caller reports itself; a /// generation cut off by the output limit is rejected so partial code is never applied. -pub(crate) fn decode_command(payload: Value) -> Result { +pub(crate) fn decode_command(payload: Value) -> Result { let request: DecodeRequest = serde_json::from_value(payload).map_err(|_| failure("AI_INLINE_EDIT_INVALID_REQUEST"))?; let protocol = WireProtocol::parse(&request.wire_protocol) diff --git a/rust/lithe-ai/src/lib.rs b/rust/lithe-ai/src/lib.rs new file mode 100644 index 00000000..51382bc3 --- /dev/null +++ b/rust/lithe-ai/src/lib.rs @@ -0,0 +1,48 @@ +//! Portable AI CLI configuration parsing and HTTP request planning; hosts own I/O and secrets. +//! +//! This crate holds every `ai.*` command of Lithe Core: commit message generation, pull +//! request descriptions, inline edits and AI CLI configuration import. It plans HTTP requests +//! and decodes provider responses; it never sends a request, reads a file or holds a +//! credential. Hosts resolve credentials, perform the request and pass the response back. +//! +//! `lithe-core` links it as a normal dependency and routes exactly [`COMMANDS`] here through +//! its JSON dispatch, so hosts reach these commands over the usual Core channel. Requests and +//! responses reuse the Core envelope: `{id, command, payload}` in and `{id, ok, data | error}` +//! out, with the same stable error codes. +//! +//! Contracts: `rust/contracts/ai-commit.md`, `rust/contracts/ai-generation-requests.md` and the +//! "AI configuration" section of `rust/contracts/rust-core-api.md`. Fixtures live in +//! `rust/fixtures/ai/`. + +mod agent_configuration; +mod commands; +mod configuration; +mod dispatch; +mod error; +mod generation; +mod inline_edit; +mod provider_request; +mod pull_request; + +pub use dispatch::{execute, execute_json, is_ai_command, COMMANDS}; +pub use error::{AiError, AiErrorCode}; +use generation::plan_commit_bounded; +pub use generation::{commit_input_budget, sensitive_path as is_sensitive_commit_path}; + +pub use configuration::{ + configuration_sources, detect, is_configured, parse_claude, parse_codex, ConfigurationFile, + ConfigurationFiles, ConfigurationSourceFile, CredentialLocator, DetectedConfiguration, +}; +pub use generation::{ + decode_message, plan_commit, ChatTokenLimitField, CommitFile, CommitOptions, CommitRequestPlan, + Provider, +}; +pub use pull_request::{ + decode_pull_request, plan_pull_request, PullRequestComparison, PullRequestDescription, + PullRequestOptions, +}; + +#[cfg(test)] +mod request_tests; +#[cfg(test)] +mod tests; diff --git a/rust/lithe-core/src/ai/provider_request.rs b/rust/lithe-ai/src/provider_request.rs similarity index 100% rename from rust/lithe-core/src/ai/provider_request.rs rename to rust/lithe-ai/src/provider_request.rs diff --git a/rust/lithe-core/src/ai/pull_request.rs b/rust/lithe-ai/src/pull_request.rs similarity index 100% rename from rust/lithe-core/src/ai/pull_request.rs rename to rust/lithe-ai/src/pull_request.rs diff --git a/rust/lithe-core/src/ai/request_tests.rs b/rust/lithe-ai/src/request_tests.rs similarity index 98% rename from rust/lithe-core/src/ai/request_tests.rs rename to rust/lithe-ai/src/request_tests.rs index 13becb5d..8cfc05d6 100644 --- a/rust/lithe-core/src/ai/request_tests.rs +++ b/rust/lithe-ai/src/request_tests.rs @@ -8,13 +8,13 @@ use serde_json::{json, Value}; fn pull_request_fixture() -> Value { serde_json::from_str(include_str!( - "../../../fixtures/ai/pull-request-description-v1.json" + "../../fixtures/ai/pull-request-description-v1.json" )) .unwrap() } fn inline_edit_fixture() -> Value { - serde_json::from_str(include_str!("../../../fixtures/ai/inline-edit-v1.json")).unwrap() + serde_json::from_str(include_str!("../../fixtures/ai/inline-edit-v1.json")).unwrap() } fn typed(fixture: &Value) -> (Provider, PullRequestOptions, PullRequestComparison) { diff --git a/rust/lithe-core/src/ai/tests.rs b/rust/lithe-ai/src/tests.rs similarity index 98% rename from rust/lithe-core/src/ai/tests.rs rename to rust/lithe-ai/src/tests.rs index 4dd6061b..1a6587e9 100644 --- a/rust/lithe-core/src/ai/tests.rs +++ b/rust/lithe-ai/src/tests.rs @@ -5,10 +5,7 @@ use serde_json::{json, Value}; use std::collections::BTreeMap; fn configuration_fixture() -> Value { - serde_json::from_str(include_str!( - "../../../fixtures/ai/cli-configuration-v1.json" - )) - .unwrap() + serde_json::from_str(include_str!("../../fixtures/ai/cli-configuration-v1.json")).unwrap() } fn execute(command: &str, payload: &Value) -> Value { @@ -195,10 +192,8 @@ fn configuration_parse_errors_are_stable_codes_without_input() { } fn fixture() -> (Provider, CommitOptions, Vec, Value) { - let value: Value = serde_json::from_str(include_str!( - "../../../fixtures/ai/commit-generation-v1.json" - )) - .unwrap(); + let value: Value = + serde_json::from_str(include_str!("../../fixtures/ai/commit-generation-v1.json")).unwrap(); ( serde_json::from_value(value["provider"].clone()).unwrap(), serde_json::from_value(value["options"].clone()).unwrap(), @@ -617,7 +612,8 @@ fn bounded_commit_evidence_preserves_truncation_and_rejects_missing_prefixes() { diff: "界".repeat(8_000), original_characters: None, }; - let plan = plan_commit_bounded(&provider, &options, &[file.clone()], &[12_000]).unwrap(); + let plan = + plan_commit_bounded(&provider, &options, std::slice::from_ref(&file), &[12_000]).unwrap(); assert!(plan.body.to_string().contains("Diff truncated")); let mut typed_file = file.clone(); typed_file.original_characters = Some(12_000); diff --git a/rust/lithe-core/Cargo.toml b/rust/lithe-core/Cargo.toml index 17aeb1fa..e84cef93 100644 --- a/rust/lithe-core/Cargo.toml +++ b/rust/lithe-core/Cargo.toml @@ -14,6 +14,7 @@ crate-type = ["rlib", "staticlib", "cdylib"] plugin-sdk-e2e = [] [dependencies] +lithe-ai = { path = "../lithe-ai" } lithe-core-rules = { path = "../lithe-core-rules" } lithe-git-host = { path = "../lithe-git-host" } lithe-ide-host = { path = "../lithe-ide-host" } diff --git a/rust/lithe-core/src/ai/mod.rs b/rust/lithe-core/src/ai/mod.rs deleted file mode 100644 index 5582a631..00000000 --- a/rust/lithe-core/src/ai/mod.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Portable AI CLI configuration parsing and HTTP request planning; hosts own I/O and secrets. - -mod agent_configuration; -mod commands; -mod configuration; -mod generation; -mod inline_edit; -mod provider_request; -mod pull_request; -pub(crate) use generation::{ - commit_input_budget, plan_commit_bounded, sensitive_path as is_sensitive_commit_path, -}; - -pub use agent_configuration::{configuration_sources_command, parse_configuration_command}; -pub use configuration::{ - configuration_sources, detect, is_configured, parse_claude, parse_codex, ConfigurationFile, - ConfigurationFiles, ConfigurationSourceFile, CredentialLocator, DetectedConfiguration, -}; -pub use generation::{ - decode_message, plan_commit, ChatTokenLimitField, CommitFile, CommitOptions, CommitRequestPlan, - Provider, -}; -pub use pull_request::{ - decode_pull_request, plan_pull_request, PullRequestComparison, PullRequestDescription, - PullRequestOptions, -}; - -#[cfg(test)] -mod request_tests; -#[cfg(test)] -mod tests; - -pub(crate) use commands::{ - commit_input_budget_command, decode_commit_command, decode_pull_request_command, - plan_commit_command, plan_pull_request_command, -}; -pub(crate) use inline_edit::{ - decode_command as decode_inline_edit_command, plan_command as plan_inline_edit_command, -}; diff --git a/rust/lithe-core/src/git/commit_message_context.rs b/rust/lithe-core/src/git/commit_message_context.rs index c6a6a3f8..ec2ba12a 100644 --- a/rust/lithe-core/src/git/commit_message_context.rs +++ b/rust/lithe-core/src/git/commit_message_context.rs @@ -99,11 +99,11 @@ fn selected_repositories(mut repositories: Vec) -> Result Result<(Vec, St } } for (file, patch) in sections { - if crate::ai::is_sensitive_commit_path(&file.path) + if lithe_ai::is_sensitive_commit_path(&file.path) || file .original_path .as_deref() - .is_some_and(crate::ai::is_sensitive_commit_path) + .is_some_and(lithe_ai::is_sensitive_commit_path) { return Err(invalid("AI_COMMIT_SENSITIVE_FILE")); } @@ -357,7 +357,7 @@ pub(crate) fn commit_message_context_command(value: Value) -> Result>(); - let budgets = crate::ai::commit_input_budget( + let budgets = lithe_ai::commit_input_budget( request .maximum_diff_characters .ok_or_else(|| invalid("AI_COMMIT_INVALID_OPTIONS"))?, diff --git a/rust/lithe-core/src/lib.rs b/rust/lithe-core/src/lib.rs index 7a4180a8..3a87ef85 100644 --- a/rust/lithe-core/src/lib.rs +++ b/rust/lithe-core/src/lib.rs @@ -1,7 +1,6 @@ //! Deterministic application services shared by the macOS and Windows hosts. mod action_search; -pub mod ai; mod build; mod community; mod debug; @@ -30,6 +29,10 @@ pub use protocol::{ CoreCommand, CoreError, CoreEvent, CoreRequest, CoreResponse, ErrorCode, ResponseData, }; +/// The `ai.*` commands, kept in their own crate (`rust/lithe-ai`); re-exported so the former +/// `lithe_core::ai` paths keep resolving. +pub use lithe_ai as ai; + /// The cancellation scope of one request, for a host crate that answers some /// commands itself and still has to honour `cancel_operation` and timeouts the /// way Core's own dispatcher does. diff --git a/rust/lithe-core/src/protocol/error.rs b/rust/lithe-core/src/protocol/error.rs index 0b1953cf..40e29e6a 100644 --- a/rust/lithe-core/src/protocol/error.rs +++ b/rust/lithe-core/src/protocol/error.rs @@ -120,6 +120,17 @@ pub fn invalid_relative_path(value: &str) -> bool { || normalized.split('/').any(|component| component == "..") } +impl From for CoreError { + /// AI errors already use Core's category names, so the mapping is one to one. + fn from(error: lithe_ai::AiError) -> Self { + let code = match error.code { + lithe_ai::AiErrorCode::InvalidRequest => ErrorCode::InvalidRequest, + lithe_ai::AiErrorCode::NotSupported => ErrorCode::NotSupported, + }; + Self::new(code, error.message) + } +} + impl From for CoreError { /// Rule errors already use Core's category names, so the mapping is one to one. fn from(error: lithe_core_rules::RuleError) -> Self { diff --git a/rust/lithe-core/src/runtime/dispatcher.rs b/rust/lithe-core/src/runtime/dispatcher.rs index 25b0e142..d96c4323 100644 --- a/rust/lithe-core/src/runtime/dispatcher.rs +++ b/rust/lithe-core/src/runtime/dispatcher.rs @@ -67,6 +67,14 @@ fn plugin_response( } } +/// Runs one `ai.*` command in `lithe-ai`. +fn ai_response(id: Option, command: &str, payload: serde_json::Value) -> CoreResponse { + match lithe_ai::execute(command, payload) { + Ok(data) => CoreResponse::success(id, data), + Err(error) => CoreResponse::failure(id, error.into()), + } +} + fn agent_response( id: Option, payload: serde_json::Value, @@ -197,33 +205,23 @@ fn execute_parsed(mut parsed: CoreRequest, document: Option<&str>) -> CoreRespon "coreVersion": env!("CARGO_PKG_VERSION") }), ), - CoreCommand::AiCommitInputBudget => { - plugin_response(id, parsed.payload, crate::ai::commit_input_budget_command) - } - CoreCommand::AiPlanCommit => { - plugin_response(id, parsed.payload, crate::ai::plan_commit_command) - } + CoreCommand::AiCommitInputBudget => ai_response(id, "ai.commitInputBudget", parsed.payload), + CoreCommand::AiPlanCommit => ai_response(id, "ai.planCommit", parsed.payload), CoreCommand::AiDecodeCommitMessage => { - plugin_response(id, parsed.payload, crate::ai::decode_commit_command) + ai_response(id, "ai.decodeCommitMessage", parsed.payload) } CoreCommand::AiConfigurationSources => { - plugin_response(id, parsed.payload, crate::ai::configuration_sources_command) + ai_response(id, "ai.configuration.sources", parsed.payload) } CoreCommand::AiConfigurationParse => { - plugin_response(id, parsed.payload, crate::ai::parse_configuration_command) - } - CoreCommand::AiPullRequestPlan => { - plugin_response(id, parsed.payload, crate::ai::plan_pull_request_command) + ai_response(id, "ai.configuration.parse", parsed.payload) } + CoreCommand::AiPullRequestPlan => ai_response(id, "ai.pullRequest.plan", parsed.payload), CoreCommand::AiPullRequestDecode => { - plugin_response(id, parsed.payload, crate::ai::decode_pull_request_command) - } - CoreCommand::AiInlineEditPlan => { - plugin_response(id, parsed.payload, crate::ai::plan_inline_edit_command) - } - CoreCommand::AiInlineEditDecode => { - plugin_response(id, parsed.payload, crate::ai::decode_inline_edit_command) + ai_response(id, "ai.pullRequest.decode", parsed.payload) } + CoreCommand::AiInlineEditPlan => ai_response(id, "ai.inlineEdit.plan", parsed.payload), + CoreCommand::AiInlineEditDecode => ai_response(id, "ai.inlineEdit.decode", parsed.payload), CoreCommand::PluginsNativeUIValidateData => plugin_response( id, parsed.payload, diff --git a/rust/lithe-core/src/tests/protocol.rs b/rust/lithe-core/src/tests/protocol.rs index 9f979ef2..16bf67f3 100644 --- a/rust/lithe-core/src/tests/protocol.rs +++ b/rust/lithe-core/src/tests/protocol.rs @@ -485,6 +485,37 @@ fn every_rules_crate_command_is_routed_by_core() { } } +#[test] +fn every_ai_crate_command_is_routed_by_core() { + for command in lithe_ai::COMMANDS { + assert!( + crate::protocol::CoreCommand::parse(command).is_some(), + "{command} is owned by lithe-ai but not registered in CoreCommand" + ); + // A null payload reaches the command's own validation, so the failure comes from + // lithe-ai with its category intact instead of Core's unsupported-command error. + let response: Value = serde_json::from_str(&execute_json( + &serde_json::json!({"id": "ai", "command": command, "payload": null}).to_string(), + )) + .unwrap(); + assert_eq!(response["ok"], false, "{command}: {response}"); + assert_eq!( + response["error"]["code"], "invalid_request", + "{command}: {response}" + ); + } + let budgets: Value = serde_json::from_str(&execute_json( + &serde_json::json!({ + "id": "ai", "command": "ai.commitInputBudget", + "payload": {"maximumDiffCharacters": 8000, "fileLengths": [20]} + }) + .to_string(), + )) + .unwrap(); + assert_eq!(budgets["ok"], true, "{budgets}"); + assert_eq!(budgets["data"]["files"][0]["characters"], 20); +} + #[test] fn terminal_link_fixtures_cross_the_json_boundary() { let fixture: Value = serde_json::from_str(include_str!( diff --git a/scripts/rust-core/README.md b/scripts/rust-core/README.md index 9b421443..66f68c3f 100644 --- a/scripts/rust-core/README.md +++ b/scripts/rust-core/README.md @@ -2,7 +2,7 @@ Run `scripts/rust-core/verify-rust-core-comments.sh` from any directory. It checks module documentation, English comments, unsafe API safety sections, exported -Rustdoc, and intra-doc links in `lithe-core` and `lithe-core-rules`. +Rustdoc, and intra-doc links in `lithe-core`, `lithe-core-rules` and `lithe-ai`. The small `comment-lexer` helper uses pinned `rustc_lexer` tokens to distinguish comments from ordinary, raw, and byte string literals. It does not parse source diff --git a/scripts/rust-core/verify-rust-core-comments.sh b/scripts/rust-core/verify-rust-core-comments.sh index f54cce7a..9ab5f96d 100755 --- a/scripts/rust-core/verify-rust-core-comments.sh +++ b/scripts/rust-core/verify-rust-core-comments.sh @@ -6,8 +6,9 @@ ROOT_DIR="$(cd -- "$SCRIPT_DIR/../.." && pwd)" # Keep this helper, Rustdoc and subsequent checks in one native build-state # directory. A desktop worktree supplies its own existing Cargo target. export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$ROOT_DIR/.artifacts/rust-core-checks}" -# lithe-core-rules is part of Core and holds reusable editor/UI rules. -SOURCE_DIRS=("$ROOT_DIR/rust/lithe-core/src" "$ROOT_DIR/rust/lithe-core-rules/src") +# lithe-core-rules is part of Core and holds reusable editor/UI rules; lithe-ai holds the +# ai.* commands. +SOURCE_DIRS=("$ROOT_DIR/rust/lithe-core/src" "$ROOT_DIR/rust/lithe-core-rules/src" "$ROOT_DIR/rust/lithe-ai/src") cd "$ROOT_DIR" rust_files=() @@ -105,7 +106,7 @@ fi # Let Rust understand visibility instead of treating pub items in private # modules as exported APIs. This covers public functions, types, variants, and # fields without requiring documentation for every internal helper. -for manifest in rust/lithe-core/Cargo.toml rust/lithe-core-rules/Cargo.toml; do +for manifest in rust/lithe-core/Cargo.toml rust/lithe-core-rules/Cargo.toml rust/lithe-ai/Cargo.toml; do cargo rustdoc --quiet \ --manifest-path "$manifest" \ --lib -- \