diff --git a/.agents/notes/implemented/architecture/2026-09-18-java-project-build-and-launch-boundary.md b/.agents/notes/implemented/architecture/2026-09-18-java-project-build-and-launch-boundary.md index 37d59f586..52ac0e159 100644 --- a/.agents/notes/implemented/architecture/2026-09-18-java-project-build-and-launch-boundary.md +++ b/.agents/notes/implemented/architecture/2026-09-18-java-project-build-and-launch-boundary.md @@ -36,6 +36,24 @@ classpath/module-path;随后 Run 模块只启动一次项目 JDK。Maven 仍 4. Rust Core 接收结构化 `javaLaunch`,生成 `project-jdk` 直启计划; 5. macOS/Windows 宿主按各自路径分隔符拼接参数并启动一个 JVM。 +Linux 第二阶段已经接入同一 Java 项目准备能力:`linux/src/lsp.rs` 只解析 +JDT LS 直启资源、Core workspace fingerprint 和请求形状;Workbench 持有唯一的 +`java` Core session,并把 `ServiceReady` 与 `projectPreparation.blocksRun` 快照 +交给 Run。Linux 路径优先使用 `LITHE_JDTLS_ROOT` / `LITHE_JDK_ROOT`,默认从 +工作区 `.artifacts/jdtls-linux` 与 `.artifacts/jdk-linux` 解析;正式资源目录 +存在但不完整时直接报可操作错误,外部 `jdtls` wrapper 只作为兼容回退。 + +`bottom_panel.rs` 对 Maven-backed `java.main` 和带 Java 入口的 Spring Boot 配置 +先等待对应 session ready,再按 Core `javaEntrypoints` → +`vscode.java.buildWorkspace` → `vscode.java.resolveClasspath` 顺序准备目标, +最后把 JDT 返回的 `{mainClass, projectName, classPaths, modulePaths}` 作为 +`javaLaunch` 传给 `runConfig.createLaunchPlan`。Linux 不扫描源码决定入口,也 +不在 UI 拼接 classpath;构建失败保留 Core 的 build diagnostics,阻止主进程, +并通过再次 Run 重试。没有 JDT 元数据时,framework Maven goal 与独立 Java +`javac -> java` 仍沿用第一阶段 Core 计划,Maven-backed `java.main` 明确显示等待 +或准备失败。Linux 仍不读取旧 `name/type/mainClass` 文档作为主路径,不用源码 +正则补入口,也不在计划失败后伪造 `mvn compile`。 + Run 和 Debug 共用同一套 Java 项目准备逻辑。配置中的 Maven 信息仍用于 JDT LS 导入、Profile、settings.xml 和项目模型;Maven 工具窗口、框架 goal、测试与显式 Maven 任务仍走 Maven 启动计划。 @@ -176,6 +194,9 @@ Run 控件,也不在 `java-run-launch` 或平台 adapter 再做一次 `blocksR - 点击运行可能需要等待 Java 语言服务 ready;构建失败会保留真实诊断,并在目标路径 仍可解析时暂停等待用户决定,而不是永久阻止启动。 - 独立 Java 文件仍遵循“`javac` 编译再运行”的既有方案,不依赖语言服务。 +- Linux Run 只把 Core 计划解析成 Linux 进程;Core 或配置失败会保留真实错误, + 不再退化为只执行 `compile`。Maven 项目里的 Java 入口在 JDT 准备完成前不会 + 启动,Linux 也不会维护第二套源码扫描结果。 - 打开大型 Maven 项目后立即点击运行,可能要先等 JDT 的项目更新结束;等待原因 会写入日志(`Java project build is waiting`),不会再因 30 秒期限而半路失败。 Windows 运行面板在准备期间先显示一行“正在等待 Java 语言服务更新并构建项目”, @@ -202,6 +223,12 @@ Run 控件,也不在 `java-run-launch` 或平台 adapter 再做一次 `blocksR 终态测试覆盖同一次启动继续、按工作区记忆选择和索引恢复。 - Windows:Run Store 把准备结果传入 Core,并分别用 `;` 拼 classpath/module-path; Run/Debug 共用的 Store 测试覆盖同一次启动继续、取消、记忆选择和索引恢复。 +- Linux:`cargo test --manifest-path linux/Cargo.toml --lib --bins` 覆盖 Core + process/Spring Boot 计划、`javac -> java`、POSIX classpath、工具链映射、 + JDT 资源排序与 payload、入口/classpath/build 结果解析、Core Java provider 门禁、 + 过期 session/sequence、pre-launch 失败门禁与完整进程组停止;测试不依赖本机 + Maven/JDK/网络。资源解析测试只创建临时目录和 fixture + JSON。 - 共享契约:`shared/contracts/rust-core-api.md` 与 `shared/contracts/application-boundary.md`,跨平台样例为 `shared/fixtures/lsp/java-build-report-v1.json`。 @@ -213,5 +240,7 @@ Run 控件,也不在 `java-run-launch` 或平台 adapter 再做一次 `blocksR `rust/lithe-core/src/lsp/interface/engine.rs` - macOS:`LanguageToolingSessionManager`、`AppModel+RunConfiguration`、`RunService` - Windows:`java-run-launch.ts`、`lsp-core-adapter.ts`、`run.store.ts` +- Linux:`linux/src/lsp.rs`、`linux/src/workbench/view.rs`、 + `linux/src/workbench/bottom_panel.rs`、`linux/src/workbench/run/` - 相关笔记: `.agents/notes/implemented/feature/2026-09-17-standalone-java-compile-then-run.md` diff --git a/.agents/notes/implemented/architecture/2026-09-25-linux-gpui-terminal-engine-reuse.md b/.agents/notes/implemented/architecture/2026-09-25-linux-gpui-terminal-engine-reuse.md new file mode 100644 index 000000000..b52b9f419 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-25-linux-gpui-terminal-engine-reuse.md @@ -0,0 +1,111 @@ +# Agent 笔记:Linux GPUI 终端的引擎复用边界 + +状态:已实现 + +## 先说结论 + +Linux 原生 GPUI 版终端不自己实现终端语义,只复用成熟上游 `alacritty_terminal` +作为“终端引擎”(负责解析 ANSI 转义序列、维护字符网格、回滚、选择、搜索), +再由本仓库把它投影成 GPUI 元素。原因是 GPUI 生态不存在可复用的成熟终端 +控件,唯一成熟且可合法分发的上游是引擎层。开发者要记住:**终端行为(滚动、 +选择、搜索、随窗缩放、reflow)只能调用 `alacritty_terminal` 的既有 API, +禁止在 `terminal.rs` 里手写第二套终端语义。** + +## 问题 + +Linux 版工作台使用 GPUI Kit 构建,底部面板需要一个嵌入式终端。 +Windows 版同类能力由前端 xterm.js 承担,xterm.js 是完整的成熟终端控件 +(自带网格渲染、滚动、选择、搜索)。GPUI 没有 DOM,无法复用 xterm.js。 + +早期实现虽然在依赖里引了 `alacritty_terminal`,但只把它当解析器使用: +渲染固定 120×30、不随窗缩放,且完全没接上游已经提供的回滚滚动、 +文本选择、复制、搜索 API。结果是终端“能跑但不好用”,并被迫在文件头 +声明“多标签、搜索、复制粘贴、随窗缩放暂不支持”。 + +## 决策 + +### 复用边界 + +- `alacritty_terminal` 0.26 是 Linux 终端的唯一引擎。解析、网格状态、 + 回滚缓冲、选择模型、正则搜索、resize/reflow 全部由它提供。 +- `linux/src/workbench/terminal.rs` 只做三件事: + 1. 用 `portable-pty` 起 PTY 会话并转发字节; + 2. 调用上游 API(`Term::resize`、`Term::scroll_display`、 + `Selection`、`RegexSearch`、`Term::renderable_content`); + 3. 把网格投影成 GPUI 元素并处理鼠标/键盘事件。 +- 渲染层实现成“固定字符单元格”排版:单元格宽度由上游 + `WindowTextSystem::em_layout_width` 按当前字号实测,行高按字号倍率换算。 + 鼠标坐标换算、PTY 行列数、网格 `cols/rows` 三者必须使用同一套 + `CellMetrics`,否则选择与点击会出现半格偏移。 + +### 正确做法 + +- 需要滚动时调用 `term.scroll_display(Scroll::Delta / Bottom)`,不要 + 自己维护偏移量。 +- 需要复制时调用 `term.selection_to_string()`,不要自己遍历网格拼字符串。 +- 需要搜索时用 `term::search::RegexSearch` + `Term::search_next`,不要 + 自己写匹配循环。 +- 容器尺寸变化时用 `Term::resize(TermDims)` 并把 `cols/rows` 同步给 PTY; + reflow 由上游完成。 +- 鼠标命中用 `viewport_to_grid_point(局部坐标, CellMetrics, TermDims, display_offset)` + 换算,回滚偏移必须叠加,保证上滚后仍命中用户看到的同一行。 + +### 不要这样做 + +- 不要在 `terminal.rs` 里手写 ANSI 解析、换行、光标移动或滚动缓冲。 +- 不要固定网格行列数;固定尺寸会让全屏 TUI(如 vim、top)拿不到正确 + 窗口大小,也无法随窗缩放。 +- 不要用渲染文本的视觉长度代替单元格数换算鼠标列号;宽字符和样式分段 + 会让两者不一致。 +- 不要为了“加个功能”把终端语义下沉到 `bottom_panel.rs` 或 `view.rs`; + 宿主只负责切换标签、转发命令和生命周期。 + +## 考虑过的备选方案 + +### 备选方案一:引入 GPUI 生态的终端控件 + +如果存在成熟的 GPUI 终端控件,可以像 xterm.js 一样整体复用,工作量最小。 +但实际盘点后不存在:本地 Cargo registry 与可访问上游都没有 +`gpui-term`、`gpui_term`、`terminal-gpui` 这类 crate;Alacritty 自身的 +渲染在 `alacritty` 二进制 crate 中,基于 OpenGL/winit,与 GPUI 的 +`Element`/`Render` 体系不兼容,无法直接嵌入。 + +### 备选方案二:复用 `viml` / `wezterm-term` / `termwiz` 引擎 + +这些是其他终端模拟器的引擎,成熟度也够。但它们与 `alacritty_terminal` +能力重叠,且现有实现已经在用 `alacritty_terminal`。为同一目标引入第二个 +引擎会增加依赖体积和双份语义,收益为零,因此不采用。 + +### 备选方案三:继续维护现有固定尺寸实现 + +改动最小,但无法满足“和 Windows/Tauri 终端一致”的产品目标:固定 +120×30、无回滚、无选择、无搜索、无缩放,属于明确的能力缺口,不是可接受 +的长期状态。 + +## 后果 + +- 收益:滚动、选择、复制粘贴、搜索、随窗缩放、reflow 全部由上游保证, + 本仓库代码量显著下降,缺陷面收敛到“投影与事件接线”。 +- 收益:单元格度量、命中换算、PTY 尺寸三处共用同一来源,跨窗口大小与 + 字体缩放行为一致。 +- 代价:渲染仍需自绘(上游无 GPUI 渲染器),固定单元格排版要求等宽 + 字体,非等宽字体下对齐会退化。 +- 代价:多标签、横向分屏、超链接点击、图形协议(Sixel/Kitty)仍未接入, + 需后续按需扩展;这些属于产品编排,不属于引擎边界。 + +## 验证 + +- `cargo test --manifest-path linux/Cargo.toml -p lithe-linux --lib` + 覆盖:`viewport_to_grid_point` 的边界/内边距/回滚偏移换算、256 色与 + 加粗高亮解算,以及引擎联通性(解析输出入网格、回滚 `display_offset`、 + 选择导出字符串、`resize` 改变列数)。 +- `cargo build --manifest-path linux/Cargo.toml -p lithe-linux` 通过。 +- `./.agents/skills/write-stable-tests/scripts/verify-test-stability.sh` 通过。 + +## 适用范围 + +- `linux/src/workbench/terminal.rs`:Linux 终端的引擎调用、渲染投影与事件接线。 +- `linux/src/i18n.rs`:终端搜索栏等新增文案键。 +- 不适用于 Windows 产品;Windows 终端由 + `windows/tauri/src/features/terminal/`(xterm.js)与 + `windows/tauri/crates/terminal/`(`portable-pty`)承担。 diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml new file mode 100644 index 000000000..fa099daf8 --- /dev/null +++ b/.github/workflows/ci-linux.yml @@ -0,0 +1,80 @@ +name: Linux CI + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: linux-ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + name: Build & Package Linux Frontend + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Check out source + uses: actions/checkout@v7 + + - name: Install system dependencies for GPUI + run: | + sudo apt-get update + sudo apt-get install -y \ + libxkbcommon-dev \ + libxkbcommon-x11-dev \ + libxcb-xkb-dev \ + libfontconfig1-dev \ + libfreetype6-dev \ + libx11-dev \ + libxcb1-dev \ + libx11-xcb-dev \ + libssl-dev \ + pkg-config + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + - name: Restore Cargo dependencies + uses: actions/cache@v6 + with: + path: | + ~/.cargo/git + ~/.cargo/registry + target + key: cargo-linux-${{ runner.os }}-${{ hashFiles('linux/Cargo.toml', 'rust/Cargo.lock') }} + restore-keys: | + cargo-linux-${{ runner.os }}- + + - name: Check formatting + run: | + cargo fmt --manifest-path linux/Cargo.toml -- --check + + - name: Check lithe-linux compilation + run: | + cargo check --manifest-path linux/Cargo.toml + + - name: Build lithe-linux binary + run: | + ./scripts/build-linux.sh --release + + - name: Package Linux application + run: | + ./scripts/package-linux.sh --release + + - name: Upload Linux artifact + uses: actions/upload-artifact@v4 + with: + name: lithe-linux-dist + path: dist/*.tar.gz + if-no-files-found: error + retention-days: 14 diff --git a/.gitignore b/.gitignore index 442d671a5..82d0575f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ .DS_Store .build/ -rust/target/ +target/ .swiftpm/ .idea/ +.vscode/ DerivedData/ /windows/build*/ dist/ @@ -15,4 +16,11 @@ docs/visual-qa/ .reasonix/ reasonix.toml .codex/ +.codegraph/ /node_modules/ +*.swp +*.swo +*~ +*.tar.gz +*.deb +*.AppImage diff --git a/linux/Cargo.lock b/linux/Cargo.lock new file mode 100644 index 000000000..375ab4c10 --- /dev/null +++ b/linux/Cargo.lock @@ -0,0 +1,9883 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "accesskit" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" +dependencies = [ + "enumn", + "uuid", +] + +[[package]] +name = "accesskit_atspi_common" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023da0e5097f46df7092d5280b02efb9bbf8d93298daeced42652463e357d636" +dependencies = [ + "accesskit", + "accesskit_consumer", + "atspi-common", + "phf 0.13.1", + "serde", + "zvariant", +] + +[[package]] +name = "accesskit_consumer" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_macos" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce02dc63b43f0c9296af9ac946312a2dc8814427d7a64d2d600971dac55b6076" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.16.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_unix" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e156ed3802e35eefe894ef2671bc6c889303d8a7e110b5e1b48f504b91362f" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "zbus", +] + +[[package]] +name = "accesskit_windows" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106c2b961215864d1c2e703ee63269c25c4e80a577ffb2c1017b9c17dcdf83a1" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.16.1", + "static_assertions", + "windows 0.62.2", + "windows-core 0.62.2", +] + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", + "zeroize", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "alacritty_terminal" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda177466b9524d59f1b12f0dd30b68696788e9992a7e959021c4a0ed96fcf59" +dependencies = [ + "base64 0.22.1", + "bitflags 2.13.2", + "home", + "libc", + "log", + "miow", + "parking_lot", + "piper", + "polling", + "regex-automata", + "rustix", + "rustix-openpty", + "serde", + "signal-hook", + "unicode-width", + "vte", + "windows-sys 0.59.0", +] + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ammonia" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f7ed3ac4252be4d3e36fb589ed5659356842587b21a31596a9dc89d3b2f5e2a" +dependencies = [ + "cssparser", + "html5ever 0.40.1", + "maplit", + "url", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "annotate-snippets" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "ashpd" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.5", + "raw-window-handle", + "serde", + "serde_repr", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + +[[package]] +name = "ashpd" +version = "0.13.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8421aaa9644a5faf26735f258b669b15f063313ef8f8e2bdb28912a1a6f111" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "getrandom 0.4.3", + "serde", + "serde_repr", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-compression" +version = "0.4.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb61aea1a7def73ee7c350a184f0e70b32c182344e2e75bf70c9b621b83417fd" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-io", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atspi" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" +dependencies = [ + "atspi-common", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "atspi-proxies" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" +dependencies = [ + "atspi-common", + "serde", + "zbus", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.20", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide 0.8.9", + "object", + "rustc-demangle", + "windows-link 0.2.1", +] + +[[package]] +name = "base62" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3a647dce9631f152a99ec16cfc09267ae55fe280d5dc288964027d458076323" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.13.2", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.3", + "shlex 1.3.0", + "syn 2.0.119", +] + +[[package]] +name = "bit-set" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "borsh" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" +dependencies = [ + "bytes", + "cfg_aliases 0.2.2", +] + +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "serde_core", +] + +[[package]] +name = "built" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "bzip2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" +dependencies = [ + "libbz2-rs-sys", +] + +[[package]] +name = "calloop" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +dependencies = [ + "bitflags 2.13.2", + "polling", + "rustix", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop", + "rustix", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "caseless" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6fd507454086c8edfd769ca6ada439193cdb209c7681712ef6275cccbfe5d8" +dependencies = [ + "unicode-normalization", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cbindgen" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" +dependencies = [ + "heck 0.4.1", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.119", + "tempfile", + "toml 0.8.23", +] + +[[package]] +name = "cc" +version = "1.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", + "zeroize", +] + +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation 0.1.2", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c" +dependencies = [ + "bitflags 2.13.2", + "block", + "cocoa-foundation 0.2.1", + "core-foundation 0.10.1", + "core-graphics 0.24.0", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" +dependencies = [ + "bitflags 2.13.2", + "block", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "objc", +] + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "compression-codecs" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef16c47ba2797aa6a909cc37d39911f3a6743811fe7408ac0b0cc0276b656e9" +dependencies = [ + "bzip2", + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e8ccc4ea9f6acc32d102c0f6d471d11d913ad15f20c04de743374861fa1d414" + +[[package]] +name = "comrak" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d5910408554659ed848ff469e67ec83b30f179e72cec286cfdae64d1616f466" +dependencies = [ + "caseless", + "emojis", + "entities", + "finl_unicode", + "jetscii", + "phf 0.13.1", + "phf_codegen 0.13.1", + "rustc-hash 2.1.3", + "smallvec", + "typed-arena", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-helmer-fork" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "core-graphics2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4416167a69126e617f8d0a214af0e3c1dbdeffcb100ddf72dcd1a1ac9893c146" +dependencies = [ + "bitflags 2.13.2", + "block", + "cfg-if", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "core-text" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130" +dependencies = [ + "core-foundation 0.10.1", + "core-graphics 0.24.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-video" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139679cc63eb9504bdbe37e37874b0247136177655f0008588781e90863afa62" +dependencies = [ + "block", + "core-foundation 0.10.1", + "core-graphics2", + "io-surface", + "libc", + "metal", +] + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "cosmic-text" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be17b688510d934ce13f48a2beba700e11583e281e0fda99c22bb256a14eda73" +dependencies = [ + "bitflags 2.13.2", + "fontdb", + "harfrust", + "linebender_resource_handle", + "log", + "rangemap", + "rustc-hash 2.1.3", + "self_cell", + "skrifa 0.40.0", + "smol_str", + "swash", + "sys-locale", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "cssparser" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11119743ad110e8c1bdccd930d7f5c30c99e5fc76a7b63ec9807e84eef0c5f59" +dependencies = [ + "dtoa-short", + "itoa", + "smallvec", +] + +[[package]] +name = "ctor" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" +dependencies = [ + "link-section", + "linktime-proc-macro", +] + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "libc", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dwrote" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b35532432acc8b19ceed096e35dfa088d3ea037fe4f3c085f1f97f33b4d02" +dependencies = [ + "lazy_static", + "libc", + "winapi", + "wio", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.6+spec-1.1.0", + "vswhom", + "winreg 0.55.0", +] + +[[package]] +name = "emojis" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4d5d50b0b58df5173d8ff1192b4d1422ceae5d981b30d4b6f8ed1d673a2bc4" +dependencies = [ + "phf 0.13.1", +] + +[[package]] +name = "encoding_rs" +version = "0.8.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "encoding_rs_io" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba3fe847045ecff794b9c138293a80db914678c453ad63fbf0c6a9eb6e00b22" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "entities" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" + +[[package]] +name = "enum-iterator" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "enumn" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etagere" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "exr" +version = "1.74.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide 0.8.9", + "num-complex", + "pulp", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +dependencies = [ + "getrandom 0.4.3", +] + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" + +[[package]] +name = "finl_unicode" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bb028c8b4148c9ee0cca68fcd9add6044e81d3619f48577ddf13a263d047a2" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "float-ord" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" + +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + +[[package]] +name = "fluent-uri" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "fastrand", + "futures-core", + "futures-sink", + "spin 0.9.9", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font-types" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "font-types" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8eb065f3251655b3c90e22e5e363f310fc5332fb3402e37bbc94752283248f6" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree 0.20.0", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "freetype-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-concurrency" +version = "7.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" +dependencies = [ + "fixedbitset", + "futures-core", + "futures-lite", + "pin-project", + "smallvec", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link 0.2.1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "globset" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + +[[package]] +name = "glow" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-allocator" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" +dependencies = [ + "ash", + "hashbrown 0.16.1", + "log", + "presser", + "thiserror 2.0.20", + "windows 0.62.2", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.13.2", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "gpui-base" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e904ee0d351f4e85b967e26ae986d3aa195a8d9d35fcd18184a2c40989694e" +dependencies = [ + "aho-corasick", + "anyhow", + "ashpd 0.13.13", + "async-channel", + "chrono", + "data-url", + "futures", + "gpui-pre", + "gpui-pre-macros", + "gpui-pre-sum-tree", + "html5ever 0.27.0", + "instant", + "lsp-types", + "markdown", + "markup5ever_rcdom", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "raw-window-handle", + "regex", + "ropey", + "schemars", + "serde", + "serde_json", + "smallvec", + "smol", + "tracing", + "unicode-segmentation", + "web-time", + "windows 0.58.0", +] + +[[package]] +name = "gpui-component" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cf6c72516b44bb2e66a1bd2ccf77a122ecd1b54a75559ac299bb296bfcb5bb7" +dependencies = [ + "anyhow", + "chrono", + "core-text", + "enum-iterator", + "gpui-base", + "gpui-component-macros", + "gpui-kit-assets", + "gpui-pre", + "gpui-pre-macros", + "gpui-pre-sum-tree", + "instant", + "itertools 0.13.0", + "log", + "lsp-types", + "markdown", + "notify", + "num-traits", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "once_cell", + "paste", + "raw-window-handle", + "regex", + "resvg 0.45.1", + "ropey", + "rust-i18n", + "schemars", + "serde", + "serde_json", + "serde_repr", + "smallvec", + "smol", + "tracing", + "tree-sitter", + "tree-sitter-bash", + "tree-sitter-java", + "tree-sitter-javascript", + "tree-sitter-json", + "tree-sitter-md", + "tree-sitter-rust", + "tree-sitter-sequel", + "tree-sitter-toml-ng", + "tree-sitter-typescript", + "tree-sitter-yaml", + "uuid", + "windows 0.58.0", +] + +[[package]] +name = "gpui-component-macros" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3867545508c7cf6a5f6da586f60d894a3f8e7cce94b7fe82df7fad5f868ec20e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui-kit" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42732dc20f50926278b2abf7a99d427a7741ab28bca6bfb49685f9ce3a53a1c7" +dependencies = [ + "gpui-base", + "gpui-component", + "gpui-kit-assets", + "gpui-pre", + "gpui-pre-platform", + "gpui-pre-web", +] + +[[package]] +name = "gpui-kit-assets" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111d3d03a68454318a193f137419864ccebdc7d3dc0194b0b0b3f818a6bc3d55" +dependencies = [ + "anyhow", + "gpui-pre", + "gpui-pre-reqwest", + "log", + "rust-embed", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "gpui-pre" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0437c0b83e636a92bd1a39fa1d05fb632ae671289537497b35871ffbe231b84" +dependencies = [ + "accesskit", + "anyhow", + "async-channel", + "async-task", + "bindgen", + "bitflags 2.13.2", + "chrono", + "core-video", + "ctor", + "derive_more", + "embed-resource", + "etagere", + "futures", + "futures-concurrency", + "getrandom 0.3.4", + "gpui-pre-collections", + "gpui-pre-http-client", + "gpui-pre-macros", + "gpui-pre-refineable", + "gpui-pre-scheduler", + "gpui-pre-shared-string", + "gpui-pre-sum-tree", + "gpui-pre-util", + "gpui-pre-util-macros", + "heapless", + "image", + "inventory", + "itertools 0.14.0", + "log", + "lyon", + "num_cpus", + "objc2-core-foundation", + "objc2-core-video", + "parking", + "parking_lot", + "pin-project", + "pollster 0.4.0", + "postage", + "profiling", + "rand 0.9.5", + "raw-window-handle", + "regex", + "resvg 0.46.0", + "schemars", + "seahash", + "serde", + "serde_json", + "slotmap", + "smallvec", + "spin 0.10.1", + "strum", + "taffy", + "thiserror 2.0.20", + "ttf-parser", + "url", + "usvg 0.46.0", + "uuid", + "waker-fn", + "web-time", + "windows 0.62.2", + "zed-font-kit", + "zed-scap", +] + +[[package]] +name = "gpui-pre-apple" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af534f0746eb9e014114bcc0ee00dc250c4b8d75df2e437180c403aa65649c1" +dependencies = [ + "anyhow", + "block", + "cbindgen", + "cocoa 0.26.1", + "core-foundation 0.10.1", + "core-video", + "derive_more", + "etagere", + "foreign-types", + "gpui-pre", + "image", + "log", + "metal", + "objc", + "objc2 0.6.4", + "parking_lot", +] + +[[package]] +name = "gpui-pre-collections" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e7a65204039187d1f4b3d4c2e0233eeac54d4acb0b13f9dc1a3b5adad26c1f" +dependencies = [ + "gpui-pre-util", + "indexmap", + "rustc-hash 2.1.3", +] + +[[package]] +name = "gpui-pre-derive-refineable" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79dfb4a05f3cd9893d7768dbf5dba1bbbe99cab7394e89b360ccb3b2bcb6b34e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui-pre-http-client" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd5a7542c77887a5aff997d78c95884119c8463409569b63fcf060956da1682" +dependencies = [ + "anyhow", + "async-compression", + "bytes", + "derive_more", + "futures", + "http", + "http-body", + "log", + "parking_lot", + "serde", + "serde_json", + "serde_urlencoded", + "url", +] + +[[package]] +name = "gpui-pre-linux" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aab655cda1d6dcd549ac869e8678babc84cf573ab6794a1fe1a39c6c70c60b48" +dependencies = [ + "accesskit", + "accesskit_unix", + "anyhow", + "as-raw-xcb-connection", + "ashpd 0.13.13", + "bitflags 2.13.2", + "bytemuck", + "calloop", + "calloop-wayland-source", + "filedescriptor", + "futures", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-http-client", + "gpui-pre-util", + "gpui-pre-wgpu", + "libc", + "log", + "notify-rust", + "oo7", + "open", + "parking_lot", + "raw-window-handle", + "smallvec", + "smol", + "strum", + "url", + "uuid", + "wayland-backend", + "wayland-client", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-plasma", + "wayland-protocols-wlr", + "x11-clipboard", + "x11rb", + "xkbcommon", + "zed-scap", + "zed-xim", +] + +[[package]] +name = "gpui-pre-macos" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951d17e41a72067ad2d35c60e08a62e8ad7fa9400b41c802e26cfb459f6c05da" +dependencies = [ + "accesskit", + "accesskit_macos", + "anyhow", + "async-task", + "block2 0.6.2", + "cocoa 0.26.1", + "core-foundation 0.10.1", + "core-foundation-sys", + "core-graphics 0.24.0", + "core-text", + "ctor", + "dispatch2", + "foreign-types", + "futures", + "gpui-pre", + "gpui-pre-apple", + "gpui-pre-collections", + "gpui-pre-util", + "image", + "itertools 0.14.0", + "libc", + "log", + "mach2", + "objc", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-graphics", + "objc2-core-media", + "objc2-foundation 0.3.2", + "objc2-screen-capture-kit", + "objc2-user-notifications", + "parking_lot", + "pathfinder_geometry", + "raw-window-handle", + "semver", + "smallvec", + "strum", + "uuid", + "zed-font-kit", +] + +[[package]] +name = "gpui-pre-macros" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6324092b40ea8ec7c327a4a28427e366a489b4d5d23b5d1bd0243fc2f4ca7a9e" +dependencies = [ + "heck 0.5.0", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui-pre-perf" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94bff26ccd8105f6206144f8dab16a2dc02e7a68f39dada02cdf492f0afd940" +dependencies = [ + "gpui-pre-collections", + "serde", + "serde_json", +] + +[[package]] +name = "gpui-pre-platform" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8423d1e0a693d2f9f16c0326bb01b5e81ec0531854d596a18476934ac6880478" +dependencies = [ + "console_error_panic_hook", + "gpui-pre", + "gpui-pre-linux", + "gpui-pre-macos", + "gpui-pre-web", + "gpui-pre-windows", +] + +[[package]] +name = "gpui-pre-refineable" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93b4b0a8d296a3ee5beb1136bc48f8167a6028eb1e243b56851dcd8c9fc6d7a1" +dependencies = [ + "gpui-pre-derive-refineable", +] + +[[package]] +name = "gpui-pre-reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05be23908e707966824f8c51a609904b7f30739e8d915e120a53c1b995ba964c" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-rustls", + "tokio-socks", + "tokio-util", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry 0.4.0", +] + +[[package]] +name = "gpui-pre-scheduler" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff7b10b3d2fc3fda24847a04436eca9531612c8414874a37ddc005f20e521e12" +dependencies = [ + "async-task", + "backtrace", + "chrono", + "flume", + "futures", + "parking_lot", + "rand 0.9.5", + "wasm_thread", + "web-time", +] + +[[package]] +name = "gpui-pre-shared-string" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe17f81bd98a0253140ea79f7f76442d7499bd8d17db302861af8e46c11d38df" +dependencies = [ + "schemars", + "serde", + "smol_str", +] + +[[package]] +name = "gpui-pre-sum-tree" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6d35e998a51d24248dad731de1f09d386cbbf46a04edde3b67f469b2ce3f16" +dependencies = [ + "gpui-pre-ztracing", + "heapless", + "log", + "rayon", + "tracing", +] + +[[package]] +name = "gpui-pre-util" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a026a30c578930d20974fd8c3d77de36223ac7ba193bd4259e7238929bc8806" +dependencies = [ + "anyhow", + "log", + "which", +] + +[[package]] +name = "gpui-pre-util-macros" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cbf79627a6afc626d4e887dad9d2d92bb069413e8cc692ba3e90bff219eb7a9" +dependencies = [ + "gpui-pre-perf", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui-pre-web" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34be433b9fcb370b1fe9a1c7fe911f0eb6058791ef8167dcf6c5a5deb624975c" +dependencies = [ + "anyhow", + "console_error_panic_hook", + "futures", + "gpui-pre", + "gpui-pre-http-client", + "gpui-pre-scheduler", + "gpui-pre-wgpu", + "js-sys", + "log", + "parking_lot", + "raw-window-handle", + "smallvec", + "unicode-properties", + "unicode-script", + "unicode-segmentation", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm_thread", + "web-sys", + "web-time", +] + +[[package]] +name = "gpui-pre-wgpu" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05515b32799b1767d69300921c1b562cd53104965211a0e8905acdc7aa18e693" +dependencies = [ + "anyhow", + "bytemuck", + "cosmic-text", + "etagere", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-util", + "itertools 0.14.0", + "log", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "swash", + "unicode-bidi", + "unicode-segmentation", + "web-sys", + "wgpu", + "zed-font-kit", +] + +[[package]] +name = "gpui-pre-windows" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27662e65bfcf4445d2cccfad91f1570ec07f4b1b5de98400a9f207fd19c464cf" +dependencies = [ + "accesskit", + "accesskit_windows", + "anyhow", + "dunce", + "etagere", + "futures", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-util", + "image", + "itertools 0.14.0", + "log", + "parking_lot", + "rand 0.9.5", + "raw-window-handle", + "smallvec", + "uuid", + "windows 0.62.2", + "windows-core 0.62.2", + "windows-numerics 0.3.1", + "windows-registry 0.6.1", +] + +[[package]] +name = "gpui-pre-zlog" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a5af0b734a391a84872eb770b3ee9830091ec2487b79d06371fe14eefb2b5d" +dependencies = [ + "anyhow", + "chrono", + "gpui-pre-collections", + "log", +] + +[[package]] +name = "gpui-pre-ztracing" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a24cb19f78c5bc3aab8ddf1e8e0bd6b402fbcb9c7f112f64ddbedef8f8768a59" +dependencies = [ + "gpui-pre-zlog", + "gpui-pre-ztracing-macro", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "gpui-pre-ztracing-macro" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1bb25850ad31b88bbdef454afb93ec4cdb81f5e200ba5b3f1a0368111780d2c" + +[[package]] +name = "granit-parser" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e20f99e46474f56bd905c56e817ebddcf377a611f94c53ac4649e4d3fa3c0cd0" +dependencies = [ + "arraydeque", + "smallvec", +] + +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "harfrust" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da2e5ae821f6e96664977bf974d6d6a2d6682f9ccee23e62ec1d134246845f9" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "core_maths", + "read-fonts 0.37.0", + "smallvec", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heapless" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac", + "markup5ever 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "html5ever" +version = "0.40.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456a1a377e608e555d22ddab27ac0114bc7a7b4199078108e34c2aeae6c9b130" +dependencies = [ + "log", + "markup5ever 0.40.0", + "memchr", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b69833ed729dc5aa7d19541d96d6cf8e9137194207a04916d658e43168402f" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif 0.14.2", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "qoi", + "ravif", + "rayon", + "tiff", + "zune-core 0.5.3", + "zune-jpeg 0.5.15", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + +[[package]] +name = "imagesize" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" + +[[package]] +name = "imgref" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e44b0a4eaa4c82f441d50a963f2d5f05a787240aeee097597033e72accfd22f" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inotify" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" +dependencies = [ + "libc", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "io-surface" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "554b8c5d64ec09a3a520fe58e4d48a73e00ff32899cdcbe32a4877afd4968b8e" +dependencies = [ + "cgl", + "core-foundation 0.10.1", + "core-foundation-sys", + "leaky-cow", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jetscii" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e" + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kqueue" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags 2.13.2", + "libc", +] + +[[package]] +name = "kurbo" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + +[[package]] +name = "kurbo" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" +dependencies = [ + "arrayvec", + "euclid", + "polycool", + "smallvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin 0.9.9", +] + +[[package]] +name = "leak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd100e01f1154f2908dfa7d02219aeab25d0b9c7fa955164192e3245255a0c73" + +[[package]] +name = "leaky-cow" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a8225d44241fd324a8af2806ba635fc7c8a7e9a7de4d5cf3ef54e71f5926fc" +dependencies = [ + "leak", +] + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libbz2-rs-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "link-section" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c29a617ce3df32c08497bdc1ab6e2376e0b17948ac166a2fbe5977c5954cd9" + +[[package]] +name = "linktime-proc-macro" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e57c38c1e860fd37c604281cdfb1dd2216977fd76a50f85ba2f388ef3219616" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lithe-core" +version = "0.1.0" +dependencies = [ + "ammonia", + "base64 0.22.1", + "comrak", + "lithe-git-host", + "quick-xml 0.37.5", + "rand 0.8.8", + "regex", + "reqwest", + "rsa", + "serde", + "serde_json", + "serde_yaml_ng", + "sha1", + "sha2 0.10.9", + "toml 1.1.6+spec-1.1.0", + "tree-sitter", + "tree-sitter-java", + "url", + "zip", +] + +[[package]] +name = "lithe-git-host" +version = "0.1.0" +dependencies = [ + "libc", + "rand 0.8.8", + "serde", + "serde_json", + "windows-sys 0.61.2", +] + +[[package]] +name = "lithe-linux" +version = "0.1.0" +dependencies = [ + "alacritty_terminal", + "anyhow", + "gpui-kit", + "libc", + "lithe-core", + "portable-pty", + "rfd", + "serde", + "serde_json", + "tokio", + "uuid", +] + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +dependencies = [ + "serde_core", + "value-bag", +] + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "lsp-types" +version = "0.97.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071" +dependencies = [ + "bitflags 1.3.2", + "fluent-uri", + "serde", + "serde_json", + "serde_repr", +] + +[[package]] +name = "lyon" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0578bdecb7d6d88987b8b2b1e3a4e2f81df9d0ece1078623324a567904e7b7" +dependencies = [ + "lyon_algorithms", + "lyon_tessellation", +] + +[[package]] +name = "lyon_algorithms" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdfa8785f95e57914ddb35e3b59994aeba6f5e79e9cfd03da1c269f010f36009" +dependencies = [ + "lyon_path", + "num-traits", +] + +[[package]] +name = "lyon_geom" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4336502e29e32af93cf2dad2214ed6003c17ceb5bd499df77b1de663b9042b92" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c463f9c428b7fc5ec885dcd39ce4aa61e29111d0e33483f6f98c74e89d8621e" +dependencies = [ + "lyon_geom", + "num-traits", +] + +[[package]] +name = "lyon_tessellation" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b8dcf906637ecef61b3c0740c7a4e7f27caeb31257cfac0cc579ce15be6005" +dependencies = [ + "float_next_after", + "lyon_path", + "num-traits", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "mac-notification-sys" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca" +dependencies = [ + "cc", + "log", + "objc2 0.6.4", + "objc2-foundation 0.3.2", + "time", + "uuid", +] + +[[package]] +name = "mach2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" +dependencies = [ + "libc", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "markdown" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb" +dependencies = [ + "serde", + "unicode-id", +] + +[[package]] +name = "markup5ever" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache 0.8.9", + "string_cache_codegen 0.5.4", + "tendril 0.4.3", +] + +[[package]] +name = "markup5ever" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab3dc68ac4a0f5719e560136778c1ee716e296030d75dbd4484e37e39e3a842" +dependencies = [ + "log", + "tendril 0.5.1", + "web_atoms", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" +dependencies = [ + "html5ever 0.27.0", + "markup5ever 0.12.1", + "tendril 0.4.3", + "xml5ever", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15" +dependencies = [ + "bitflags 2.13.2", + "block", + "core-graphics-types 0.2.0", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "miow" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "multiversion" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" +dependencies = [ + "multiversion-macros", +] + +[[package]] +name = "multiversion-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn 3.0.6", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "naga" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bf919621e7975acb27d881bae2fb993e0d45c8e0446e85e6272971e00dc8df" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases 0.2.2", + "codespan-reporting", + "half", + "hashbrown 0.16.1", + "hexf-parse", + "indexmap", + "libm", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.20", + "unicode-ident", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases 0.1.1", + "libc", +] + +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "normpath" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b11ce00d2594068e8a27c9146fdc5cf9f3ac38eb42c7cd34d05fea618873ac9f" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "notify" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" +dependencies = [ + "bitflags 2.13.2", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.52.0", +] + +[[package]] +name = "notify-rust" +version = "4.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b4c1b4f2aa9f25f63a7a49d3dd0ed567b3670da15330a66b29434be899b891" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + +[[package]] +name = "notify-types" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174" +dependencies = [ + "instant", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.8", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-bigint-dig" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f9a86e097b0d187ad0e65667c2f58b9254671e86e7dbb78036b16692eae099" +dependencies = [ + "libm", + "num-integer", + "num-iter", + "num-traits", + "once_cell", + "rand 0.9.5", + "serde", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "bytemuck", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "libc", + "objc2 0.6.4", + "objc2-cloud-kit", + "objc2-core-data 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image 0.3.2", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2 0.6.4", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-media" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ec576860167a15dd9fce7fbee7512beb4e31f532159d3482d1f9c6caedf31d" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.4", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-core-video", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "libc", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", +] + +[[package]] +name = "objc2-screen-capture-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74b7c5390f477482f001bc354d6571a70db7e4f8d5288e860c45521fbce11394" +dependencies = [ + "block2 0.6.2", + "dispatch2", + "objc2 0.6.4", + "objc2-core-graphics", + "objc2-core-media", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-location", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oo7" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f2bfed90f1618b4b48dcad9307f25e14ae894e2949642c87c351601d62cebd" +dependencies = [ + "aes", + "ashpd 0.13.13", + "async-fs", + "async-io", + "async-lock", + "blocking", + "cbc", + "cipher", + "digest 0.10.7", + "endi", + "futures-lite", + "futures-util", + "getrandom 0.4.3", + "hkdf", + "hmac", + "md-5", + "num", + "num-bigint-dig 0.9.1", + "pbkdf2", + "serde", + "serde_bytes", + "sha2 0.10.9", + "subtle", + "zbus", + "zbus_macros", + "zeroize", + "zvariant", +] + +[[package]] +name = "open" +version = "5.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc" +dependencies = [ + "is-wsl", + "libc", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-float" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "pathfinder_geometry" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" +dependencies = [ + "log", + "pathfinder_simd", +] + +[[package]] +name = "pathfinder_simd" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4500030c302e4af1d423f36f3b958d1aecb6c04184356ed5a833bf6b60435777" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" +dependencies = [ + "phf_shared 0.14.0", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_codegen" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b585a510fb76fdebead6897982ef2a03a21d8e6cbcca904999742a4afc6ffe" +dependencies = [ + "phf_generator 0.14.0", + "phf_shared 0.14.0", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.8", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" +dependencies = [ + "fastrand", + "phf_shared 0.14.0", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "portable-pty" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "downcast-rs", + "filedescriptor", + "lazy_static", + "libc", + "log", + "nix", + "serial2", + "shared_library", + "shell-words", + "winapi", + "winreg 0.10.1", +] + +[[package]] +name = "postage" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" +dependencies = [ + "atomic", + "crossbeam-queue", + "futures", + "log", + "parking_lot", + "pin-project", + "pollster 0.2.5", + "static_assertions", + "thiserror 1.0.69", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.15+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pulp" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "paste", + "pulp-wasm-simd-flag", + "raw-cpuid", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp-wasm-simd-flag" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases 0.2.2", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.3", + "rustls", + "socket2", + "thiserror 2.0.20", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.3", + "rand_pcg", + "ring", + "rustc-hash 2.1.3", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.20", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases 0.2.2", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "rangemap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d" + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.14.0", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.5", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.20", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "read-fonts" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" +dependencies = [ + "bytemuck", + "core_maths", + "font-types 0.11.3", +] + +[[package]] +name = "read-fonts" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" +dependencies = [ + "bytemuck", + "font-types 0.12.5", + "once_cell", +] + +[[package]] +name = "reborrow" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.20", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "resvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" +dependencies = [ + "gif 0.13.3", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes 0.15.3", + "tiny-skia", + "usvg 0.45.1", + "zune-jpeg 0.4.21", +] + +[[package]] +name = "resvg" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b563218631706d614e23059436526d005b50ab5f2d506b55a17eb65c5eb83419" +dependencies = [ + "gif 0.14.2", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes 0.16.1", + "tiny-skia", + "usvg 0.46.0", + "zune-jpeg 0.5.15", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "ashpd 0.11.1", + "block2 0.6.2", + "dispatch2", + "js-sys", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "pollster 0.4.0", + "raw-window-handle", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ropey" +version = "2.0.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4045a00dc327d084a2bbf126976e14125b54f23bd30511d45b842eba76c52d74" +dependencies = [ + "str_indices", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "roxmltree" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig 0.8.6", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust-embed" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097" +dependencies = [ + "mime_guess", + "proc-macro2", + "quote", + "rust-embed-utils", + "shellexpand", + "syn 2.0.119", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0" +dependencies = [ + "globset", + "sha2 0.11.0", + "walkdir", +] + +[[package]] +name = "rust-i18n" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c43fd69c20da13756643a5bf51ae799965465c78e05c57a1242a736aaa56527" +dependencies = [ + "globwalk", + "regex", + "rust-i18n-macro", + "rust-i18n-support", + "smallvec", +] + +[[package]] +name = "rust-i18n-macro" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "086ec8a9eaa6afda33919b9bc1661c1f4e017c40e99bbd33ca98f6e1cb6ca91f" +dependencies = [ + "glob", + "proc-macro2", + "quote", + "rust-i18n-support", + "serde", + "serde_json", + "syn 2.0.119", +] + +[[package]] +name = "rust-i18n-support" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac02dcb9a01ec145d0a5534ea9554a966050ca250b346bb2a9ab0fcdafab5979" +dependencies = [ + "arc-swap", + "base62", + "globwalk", + "itertools 0.11.0", + "normpath", + "serde", + "serde-saphyr", + "serde_json", + "siphasher", + "toml 0.8.23", + "triomphe", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustix-openpty" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de16c7c59892b870a6336f185dc10943517f1327447096bbb7bb32cd85e2393" +dependencies = [ + "errno", + "libc", + "rustix", +] + +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "indexmap", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 3.0.6", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "screencapturekit" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5eeeb57ac94960cfe5ff4c402be6585ae4c8d29a2cf41b276048c2e849d64e" +dependencies = [ + "screencapturekit-sys", +] + +[[package]] +name = "screencapturekit-sys" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22411b57f7d49e7fe08025198813ee6fd65e1ee5eff4ebc7880c12c82bde4c60" +dependencies = [ + "block", + "dispatch", + "objc", + "objc-foundation", + "objc_id", + "once_cell", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "self_cell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-saphyr" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8050abb251097357e24aff63ba2c52a6309ecb7d23a5474023df960a02694d8" +dependencies = [ + "annotate-snippets", + "base64 0.23.1", + "encoding_rs_io", + "granit-parser", + "nohash-hasher", + "num-traits", + "serde_core", + "smallvec", + "zmij", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_derive_internals" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_fmt" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e497af288b3b95d067a23a4f749f2861121ffcb2f6d8379310dcda040c345ed" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml_ng" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "serial2" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16809bc35793b19ce4e0c53924bc0dce3937f15487997cfdaed936004180730" +dependencies = [ + "cfg-if", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shared_library" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" +dependencies = [ + "lazy_static", + "libc", +] + +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + +[[package]] +name = "shellexpand" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" +dependencies = [ + "dirs 6.0.0", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "skrifa" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" +dependencies = [ + "bytemuck", + "read-fonts 0.37.0", +] + +[[package]] +name = "skrifa" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" +dependencies = [ + "bytemuck", + "read-fonts 0.41.0", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" + +[[package]] +name = "smol" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", +] + +[[package]] +name = "smol_str" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" +dependencies = [ + "borsh", + "serde_core", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spirv" +version = "0.4.0+sdk-1.4.341.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "str_indices" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6" + +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffa8a5dbe8b3f0bbe29d4c3225daafaeead63afdc1b65fc4c01a1384166038e6" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.14.0", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "string_cache_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928dcdf75e47626b3617a976ec205d9f057584c371c1f23b782129268d0e6edc" +dependencies = [ + "phf_generator 0.14.0", + "phf_shared 0.14.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "sval" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b81b254da21fe1fcc4e3a74fe39b46e25e3a863078f8b71c954d47f84889dbc6" + +[[package]] +name = "sval_buffer" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50be352d2822ffafb59e3e2ddac9d5ee60f2eeadbb7b5a2a951b9f3651e87a6f" +dependencies = [ + "sval", + "sval_ref", + "zerocopy", +] + +[[package]] +name = "sval_dynamic" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048ca293b998d9a45659159f94a64063791e74cdc670164943dbb434405573d" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_fmt" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b5888e40f80568733217f27b7317b845f463400ced36c424b1a804730e53b2" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_json" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17664d6bb6b74947afaab9d7c991caa9bf5638d4dee16fcbef637f440796049" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_nested" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c059969ca5ca163ea7fef6c9661758973d17691aba92abdcf5c428f4ec122c" +dependencies = [ + "sval", + "sval_buffer", + "sval_ref", +] + +[[package]] +name = "sval_ref" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d6b29ff568c85c87561807f51d2adfff4b6016c6363133f7cd1652a12548f3" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_serde" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f33ec9edc42b12764d5c90ca0a1d84189c6bde81ed27507f1e661c6e4e05853" +dependencies = [ + "serde_core", + "sval", + "sval_nested", +] + +[[package]] +name = "svg_fmt" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" + +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo 0.11.3", + "siphasher", +] + +[[package]] +name = "svgtypes" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d" +dependencies = [ + "kurbo 0.13.1", + "siphasher", +] + +[[package]] +name = "swash" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e" +dependencies = [ + "skrifa 0.44.0", + "yazi", + "zeno", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "sysinfo" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.57.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "taffy" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c034e05f6ee85a12daa63863c2245797715075c70649947aa0da54f3f2ab1d0f" +dependencies = [ + "arrayvec", + "serde", + "slotmap", + "smallvec", +] + +[[package]] +name = "tao-core-video-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "objc", +] + +[[package]] +name = "tauri-winrt-notification" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed071c670382e85fc2f48ae706492d8c338f4f89bf72520d32f8abfe880aade" +dependencies = [ + "thiserror 2.0.20", + "windows 0.61.3", + "windows-version", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg 0.5.15", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png 0.17.16", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-socks" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e2948f60dbe26b35f2c7fb74ac2854c1fddded0fe9d7548fcc674a246f7615" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml" +version = "1.1.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.15+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.2", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tree-sitter" +version = "0.26.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ebdd3a5a7e28a1890b876fdbd0c3c0fe0a6336cffaa104f11b9f720c9daa29" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "serde_json", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-bash" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "329a4d48623ac337d42b1df84e81a1c9dbb2946907c102ca72db158c1964a52e" +dependencies = [ + "cc", + "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-javascript" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf40bf599e0416c16c125c3cec10ee5ddc7d1bb8b0c60fa5c4de249ad34dc1b1" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-json" +version = "0.24.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d727acca406c0020cffc6cf35516764f36c8e3dc4408e5ebe2cb35a947ec471" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0d1bf6fdd806e43ae5198f82f527056d359def39e54e67a0f478ac09dac081" + +[[package]] +name = "tree-sitter-md" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efd398be546456c814598ee56c0f51769a77241511b4a58077815d120afa882" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-rust" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439e577dbe07423ec2582ac62c7531120dbfccfa6e5f92406f93dd271a120e45" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-sequel" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d198ad3c319c02e43c21efa1ec796b837afcb96ffaef1a40c1978fbdcec7d17" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-toml-ng" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9adc2c898ae49730e857d75be403da3f92bb81d8e37a2f918a08dd10de5ebb1" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-typescript" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5f76ed8d947a75cc446d5fccd8b602ebf0cde64ccf2ffa434d873d7a575eff" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-yaml" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c223db85f05e34794f065454843b0668ebc15d240ada63e2b5939f43ce7c97" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "triomphe" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b40688ea6389c8171614b25491f71d4a27946e0c7ce2da1c6de27e25abf1a0ae" +dependencies = [ + "arc-swap", + "serde", + "stable_deref_trait", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + +[[package]] +name = "unicode-id" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65dd75ab2d9d379dbe86d4059e91d8b5a6e7054865eaba1ddbfc7a817ff96392" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "usvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" +dependencies = [ + "base64 0.22.1", + "data-url", + "flate2", + "fontdb", + "imagesize 0.13.0", + "kurbo 0.11.3", + "log", + "pico-args", + "roxmltree 0.20.0", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes 0.15.3", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "usvg" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e419dff010bb12512b0ae9e3d2f318dfbdf0167fde7eb05465134d4e8756076f" +dependencies = [ + "base64 0.22.1", + "data-url", + "flate2", + "fontdb", + "imagesize 0.14.0", + "kurbo 0.13.1", + "log", + "pico-args", + "roxmltree 0.21.1", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes 0.16.1", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "sha1_smol", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "value-bag" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2799ffb329a792ecfd902b71306c8a815a6ef1c0470fa9953a6aa4d4cecbe511" +dependencies = [ + "value-bag-serde1", + "value-bag-sval2", +] + +[[package]] +name = "value-bag-serde1" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0941feceafbe7a8f59ea1096d45b97002884a41306315ad797b3684b63a81d8c" +dependencies = [ + "erased-serde", + "serde_core", + "serde_fmt", +] + +[[package]] +name = "value-bag-sval2" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839752af8179287d27eb2b94164641b1ede9e60ab7424163388dc21ebd0508cd" +dependencies = [ + "sval", + "sval_buffer", + "sval_dynamic", + "sval_fmt", + "sval_json", + "sval_ref", + "sval_serde", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "vte" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd" +dependencies = [ + "arrayvec", + "bitflags 2.13.2", + "cursor-icon", + "log", + "memchr", + "serde", +] + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasm_thread" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7516db7f32decdadb1c3b8deb1b7d78b9df7606c5cc2f6241737c2ab3a0258e" +dependencies = [ + "futures", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "log", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.13.2", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml 0.41.0", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7572660c8890448ba236b7376f27e389c6a7e1c70195622faced601f855c0ada" +dependencies = [ + "phf 0.14.0", + "phf_codegen 0.14.0", + "string_cache 0.11.0", + "string_cache_codegen 0.11.2", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wgpu" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e8840e1ba2881d4cbb18d2147627a56af426ff064c0401eb0c8410c6325d07" +dependencies = [ + "arrayvec", + "bitflags 2.13.2", + "bytemuck", + "cfg-if", + "cfg_aliases 0.2.2", + "document-features", + "hashbrown 0.16.1", + "js-sys", + "log", + "naga", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f519832254e56965a9940c4af57dcb75f702b6f6fa4a0b172f685395843a4d7" +dependencies = [ + "arrayvec", + "bit-set", + "bit-vec", + "bitflags 2.13.2", + "bytemuck", + "cfg_aliases 0.2.2", + "document-features", + "hashbrown 0.16.1", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.20", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-wasm", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-naga-bridge", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e39e26c4c0e07589e67d18546cf79ff45383659fc72fca4dd293358a0347f3" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01e09be551dc939498bdd5f6b2c66e55ab275dad25825267a08605a80fc9f0af" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-wasm" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1fb1798be2a912497d4c224f72d39bb0cb34af50e8bcc29865bc339c943059" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e592c1bbef6ad047647ae6e666ebd8cee7a32bb4544d9700ec96cbf73230257" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ace1c17727311c22a46e4e3faf56ea6de81af99dcc839bdfb54857b94d448d" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.13.2", + "block2 0.6.2", + "bytemuck", + "cfg-if", + "cfg_aliases 0.2.2", + "glow", + "glutin_wgl_sys", + "gpu-allocator", + "gpu-descriptor", + "hashbrown 0.16.1", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "naga", + "ndk-sys", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "raw-window-metal", + "renderdoc-sys", + "smallvec", + "thiserror 2.0.20", + "wasm-bindgen", + "wayland-sys", + "web-sys", + "wgpu-naga-bridge", + "wgpu-types", + "windows 0.62.2", + "windows-core 0.62.2", + "windows-result 0.4.1", +] + +[[package]] +name = "wgpu-naga-bridge" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95226013f547544b223281cd16a4fb549aa9dcb562adbda0faae4c73ffbbc161" +dependencies = [ + "naga", + "wgpu-types", +] + +[[package]] +name = "wgpu-types" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84bf84cd9ca8ca45e2b223a3868f1adf9bfc0c66aeac212e76ee7e40fdadf8f5" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "js-sys", + "log", + "raw-window-handle", + "web-sys", +] + +[[package]] +name = "which" +version = "8.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae2f2b2b816647a1cab1acc91f5bd20812d53cb344382635ec2181940c8034f" +dependencies = [ + "libc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-capture" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4df73e95feddb9ec1a7e9c2ca6323b8c97d5eeeff78d28f1eccdf19c882b24" +dependencies = [ + "parking_lot", + "rayon", + "thiserror 2.0.20", + "windows 0.61.3", + "windows-future 0.2.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result 0.3.4", + "windows-strings 0.3.1", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-clipboard" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662d74b3d77e396b8e5beb00b9cad6a9eccf40b2ef68cc858784b14c41d535a3" +dependencies = [ + "libc", + "x11rb", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "rustix", + "x11rb-protocol", + "xcursor", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcb" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6c2ad15e0e922856ee89afe862b8992334bbe7953adad56cd1199358cb30566" +dependencies = [ + "bitflags 2.13.2", + "libc", + "quick-xml 0.41.0", + "x11", +] + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xim-ctext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac61a7062c40f3c37b6e82eeeef835d5cc7824b632a72784a89b3963c33284c" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "xim-parser" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dcee45f89572d5a65180af3a84e7ddb24f5ea690a6d3aa9de231281544dd7b7" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "xkbcommon" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d66ca9352cbd4eecbbc40871d8a11b4ac8107cfc528a6e14d7c19c69d0e1ac9" +dependencies = [ + "as-raw-xcb-connection", + "libc", + "memmap2", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "xml5ever" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" +dependencies = [ + "log", + "mac", + "markup5ever 0.12.1", +] + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8b8abf912b9a29ff112e1671c97c33636903d13a69712037190e6805af4f76" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.4", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus-lockstep" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" +dependencies = [ + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus-lockstep", + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.6", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow 1.0.4", + "zvariant", +] + +[[package]] +name = "zbus_xml" +version = "5.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a" +dependencies = [ + "serde", + "winnow 1.0.4", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zcheapstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", +] + +[[package]] +name = "zed-font-kit" +version = "0.14.1-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3898e450f36f852edda72e3f985c34426042c4951790b23b107f93394f9bff5" +dependencies = [ + "bitflags 2.13.2", + "byteorder", + "core-foundation 0.10.1", + "core-graphics 0.24.0", + "core-text", + "dirs 5.0.1", + "dwrote", + "float-ord", + "freetype-sys", + "lazy_static", + "libc", + "log", + "pathfinder_geometry", + "pathfinder_simd", + "walkdir", + "winapi", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "zed-scap" +version = "0.0.8-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b338d705ae33a43ca00287c11129303a7a0aa57b101b72a1c08c863f698ac8" +dependencies = [ + "anyhow", + "cocoa 0.25.0", + "core-graphics-helmer-fork", + "log", + "objc", + "rand 0.8.8", + "screencapturekit", + "screencapturekit-sys", + "sysinfo", + "tao-core-video-sys", + "windows 0.61.3", + "windows-capture", + "x11", + "xcb", +] + +[[package]] +name = "zed-xim" +version = "0.4.0-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0b46ed118eba34d9ba53d94ddc0b665e0e06a2cf874cfa2dd5dec278148642" +dependencies = [ + "ahash", + "hashbrown 0.14.5", + "log", + "x11rb", + "xim-ctext", + "xim-parser", +] + +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + +[[package]] +name = "zerocopy" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap", + "memchr", + "thiserror 2.0.20", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core 0.5.3", +] + +[[package]] +name = "zvariant" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147" +dependencies = [ + "endi", + "enumflags2", + "serde", + "serde_bytes", + "url", + "winnow 1.0.4", + "zcheapstr", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.6", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.6", + "winnow 1.0.4", +] diff --git a/linux/Cargo.toml b/linux/Cargo.toml new file mode 100644 index 000000000..d5365b396 --- /dev/null +++ b/linux/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "lithe-linux" +version = "0.1.0" +edition = "2021" + +[dependencies] +rfd = "0.16" +gpui-kit = { version = "0.6", features = [ + "tree-sitter-bash", + "tree-sitter-java", + "tree-sitter-javascript", + "tree-sitter-markdown", + "tree-sitter-markdown-inline", + "tree-sitter-rust", + "tree-sitter-sql", + "tree-sitter-toml", + "tree-sitter-tsx", + "tree-sitter-typescript", + "tree-sitter-yaml", +] } +lithe-core = { path = "../rust/lithe-core" } +portable-pty = "0.9" +alacritty_terminal = "0.26" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["sync", "macros", "rt"] } +anyhow = "1" +uuid = { version = "1", features = ["v4"] } +libc = "0.2" diff --git a/linux/assets/file-icons/icons/expui/fileTypes/Csharp.svg b/linux/assets/file-icons/icons/expui/fileTypes/Csharp.svg new file mode 100644 index 000000000..cc74ead1e --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/Csharp.svg @@ -0,0 +1,3 @@ + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/Csharp_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/Csharp_dark.svg new file mode 100644 index 000000000..c0b2a9df3 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/Csharp_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/archive.svg b/linux/assets/file-icons/icons/expui/fileTypes/archive.svg new file mode 100644 index 000000000..87bbbeece --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/archive.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/archive_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/archive_dark.svg new file mode 100644 index 000000000..ab3cae2cc --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/archive_dark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/c.svg b/linux/assets/file-icons/icons/expui/fileTypes/c.svg new file mode 100644 index 000000000..b7a663f5a --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/c.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/c_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/c_dark.svg new file mode 100644 index 000000000..af5455250 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/c_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/cpp.svg b/linux/assets/file-icons/icons/expui/fileTypes/cpp.svg new file mode 100644 index 000000000..e8c8c1949 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/cpp.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/cpp_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/cpp_dark.svg new file mode 100644 index 000000000..3c4442388 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/cpp_dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/css.svg b/linux/assets/file-icons/icons/expui/fileTypes/css.svg new file mode 100644 index 000000000..929203773 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/css.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/css_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/css_dark.svg new file mode 100644 index 000000000..f541ee8c7 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/css_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/csv.svg b/linux/assets/file-icons/icons/expui/fileTypes/csv.svg new file mode 100644 index 000000000..cc5fa2eaa --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/csv.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/csv_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/csv_dark.svg new file mode 100644 index 000000000..d038177a9 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/csv_dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/docker.svg b/linux/assets/file-icons/icons/expui/fileTypes/docker.svg new file mode 100644 index 000000000..a4f9cab91 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/docker.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/docker_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/docker_dark.svg new file mode 100644 index 000000000..5369a264a --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/docker_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/editorConfig.svg b/linux/assets/file-icons/icons/expui/fileTypes/editorConfig.svg new file mode 100644 index 000000000..547bba721 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/editorConfig.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/editorConfig_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/editorConfig_dark.svg new file mode 100644 index 000000000..528d02ebe --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/editorConfig_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/font.svg b/linux/assets/file-icons/icons/expui/fileTypes/font.svg new file mode 100644 index 000000000..9e187b36f --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/font.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/font_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/font_dark.svg new file mode 100644 index 000000000..94fbd32d9 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/font_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/gitignore.svg b/linux/assets/file-icons/icons/expui/fileTypes/gitignore.svg new file mode 100644 index 000000000..86228c617 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/gitignore.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/gradle.svg b/linux/assets/file-icons/icons/expui/fileTypes/gradle.svg new file mode 100644 index 000000000..29f3e59ee --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/gradle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/gradle_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/gradle_dark.svg new file mode 100644 index 000000000..37c055abf --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/gradle_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/graphql.svg b/linux/assets/file-icons/icons/expui/fileTypes/graphql.svg new file mode 100644 index 000000000..d4dd36ac3 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/graphql.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/graphql_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/graphql_dark.svg new file mode 100644 index 000000000..a0095957d --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/graphql_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/groovy.svg b/linux/assets/file-icons/icons/expui/fileTypes/groovy.svg new file mode 100644 index 000000000..671aff55e --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/groovy.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/groovy_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/groovy_dark.svg new file mode 100644 index 000000000..0dfe6a462 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/groovy_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/h.svg b/linux/assets/file-icons/icons/expui/fileTypes/h.svg new file mode 100644 index 000000000..1905b901a --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/h.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/h_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/h_dark.svg new file mode 100644 index 000000000..935071870 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/h_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/html.svg b/linux/assets/file-icons/icons/expui/fileTypes/html.svg new file mode 100644 index 000000000..c446cf39e --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/html.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/html_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/html_dark.svg new file mode 100644 index 000000000..9e10d57f1 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/html_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/http.svg b/linux/assets/file-icons/icons/expui/fileTypes/http.svg new file mode 100644 index 000000000..906237407 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/http.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/image.svg b/linux/assets/file-icons/icons/expui/fileTypes/image.svg new file mode 100644 index 000000000..9a09720fa --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/image.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/image_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/image_dark.svg new file mode 100644 index 000000000..4f189a0e3 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/image_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/java.svg b/linux/assets/file-icons/icons/expui/fileTypes/java.svg new file mode 100644 index 000000000..00e1a4c96 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/java.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/javaScript.svg b/linux/assets/file-icons/icons/expui/fileTypes/javaScript.svg new file mode 100644 index 000000000..d7eb6cc8d --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/javaScript.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/javaScript_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/javaScript_dark.svg new file mode 100644 index 000000000..188617076 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/javaScript_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/java_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/java_dark.svg new file mode 100644 index 000000000..155295f59 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/java_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/json.svg b/linux/assets/file-icons/icons/expui/fileTypes/json.svg new file mode 100644 index 000000000..d8f707dd5 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/json.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/json_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/json_dark.svg new file mode 100644 index 000000000..29371ab71 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/json_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/jsp.svg b/linux/assets/file-icons/icons/expui/fileTypes/jsp.svg new file mode 100644 index 000000000..564a2190c --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/jsp.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/jsp_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/jsp_dark.svg new file mode 100644 index 000000000..1c04aa9cf --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/jsp_dark.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/markdown.svg b/linux/assets/file-icons/icons/expui/fileTypes/markdown.svg new file mode 100644 index 000000000..c603fd236 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/markdown.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/markdown_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/markdown_dark.svg new file mode 100644 index 000000000..2d86cd368 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/markdown_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/maven.svg b/linux/assets/file-icons/icons/expui/fileTypes/maven.svg new file mode 100644 index 000000000..e22b73598 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/maven.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/maven_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/maven_dark.svg new file mode 100644 index 000000000..8f8f35e5c --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/maven_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/patch.svg b/linux/assets/file-icons/icons/expui/fileTypes/patch.svg new file mode 100644 index 000000000..b3a1d040b --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/patch.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/patch_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/patch_dark.svg new file mode 100644 index 000000000..e6d13133e --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/patch_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/perl.svg b/linux/assets/file-icons/icons/expui/fileTypes/perl.svg new file mode 100644 index 000000000..565044076 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/perl.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/perl_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/perl_dark.svg new file mode 100644 index 000000000..870ac8faa --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/perl_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/powershell.svg b/linux/assets/file-icons/icons/expui/fileTypes/powershell.svg new file mode 100644 index 000000000..1039d72db --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/powershell.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/powershell_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/powershell_dark.svg new file mode 100644 index 000000000..2b8307594 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/powershell_dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/properties.svg b/linux/assets/file-icons/icons/expui/fileTypes/properties.svg new file mode 100644 index 000000000..547bba721 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/properties.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/properties_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/properties_dark.svg new file mode 100644 index 000000000..528d02ebe --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/properties_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/rst.svg b/linux/assets/file-icons/icons/expui/fileTypes/rst.svg new file mode 100644 index 000000000..09c5b340e --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/rst.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/rst_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/rst_dark.svg new file mode 100644 index 000000000..ae54b4de1 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/rst_dark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/shell.svg b/linux/assets/file-icons/icons/expui/fileTypes/shell.svg new file mode 100644 index 000000000..0676afdd7 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/shell.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/shell_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/shell_dark.svg new file mode 100644 index 000000000..7022d0c76 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/shell_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/sql.svg b/linux/assets/file-icons/icons/expui/fileTypes/sql.svg new file mode 100644 index 000000000..a57c04270 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/sql.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/sql_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/sql_dark.svg new file mode 100644 index 000000000..57d62ee80 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/sql_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/swiftLang.svg b/linux/assets/file-icons/icons/expui/fileTypes/swiftLang.svg new file mode 100644 index 000000000..4b6e68b65 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/swiftLang.svg @@ -0,0 +1,3 @@ + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/swiftLang_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/swiftLang_dark.svg new file mode 100644 index 000000000..5d386f489 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/swiftLang_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/terraform.svg b/linux/assets/file-icons/icons/expui/fileTypes/terraform.svg new file mode 100644 index 000000000..ac3ca7839 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/terraform.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/linux/assets/file-icons/icons/expui/fileTypes/terraform_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/terraform_dark.svg new file mode 100644 index 000000000..4c4d9bfee --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/terraform_dark.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/linux/assets/file-icons/icons/expui/fileTypes/text.svg b/linux/assets/file-icons/icons/expui/fileTypes/text.svg new file mode 100644 index 000000000..110139cf6 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/text.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/text_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/text_dark.svg new file mode 100644 index 000000000..86468276b --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/text_dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/toml.svg b/linux/assets/file-icons/icons/expui/fileTypes/toml.svg new file mode 100644 index 000000000..10789f0e9 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/toml.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/toml_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/toml_dark.svg new file mode 100644 index 000000000..b9e6aea69 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/toml_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/typeScript.svg b/linux/assets/file-icons/icons/expui/fileTypes/typeScript.svg new file mode 100644 index 000000000..b4d487f0d --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/typeScript.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/typeScript_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/typeScript_dark.svg new file mode 100644 index 000000000..5c74ff7f7 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/typeScript_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/vue.svg b/linux/assets/file-icons/icons/expui/fileTypes/vue.svg new file mode 100644 index 000000000..8625428aa --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/vue.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/wsdl.svg b/linux/assets/file-icons/icons/expui/fileTypes/wsdl.svg new file mode 100644 index 000000000..a92e9c5c0 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/wsdl.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/wsdl_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/wsdl_dark.svg new file mode 100644 index 000000000..3732545ba --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/wsdl_dark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/xhtml.svg b/linux/assets/file-icons/icons/expui/fileTypes/xhtml.svg new file mode 100644 index 000000000..94b886f9b --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/xhtml.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/xhtml_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/xhtml_dark.svg new file mode 100644 index 000000000..432f62e24 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/xhtml_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/xml.svg b/linux/assets/file-icons/icons/expui/fileTypes/xml.svg new file mode 100644 index 000000000..154c1eded --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/xml.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/xml_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/xml_dark.svg new file mode 100644 index 000000000..8a2eb779d --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/xml_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/xsd.svg b/linux/assets/file-icons/icons/expui/fileTypes/xsd.svg new file mode 100644 index 000000000..0c87edc93 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/xsd.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/xsd_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/xsd_dark.svg new file mode 100644 index 000000000..c012bd1e6 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/xsd_dark.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/yaml.svg b/linux/assets/file-icons/icons/expui/fileTypes/yaml.svg new file mode 100644 index 000000000..9d4031ee2 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/yaml.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/fileTypes/yaml_dark.svg b/linux/assets/file-icons/icons/expui/fileTypes/yaml_dark.svg new file mode 100644 index 000000000..b173b95d9 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/fileTypes/yaml_dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/annotation.svg b/linux/assets/file-icons/icons/expui/nodes/annotation.svg new file mode 100644 index 000000000..842956af7 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/annotation.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/annotation_dark.svg b/linux/assets/file-icons/icons/expui/nodes/annotation_dark.svg new file mode 100644 index 000000000..66abc9160 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/annotation_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/class.svg b/linux/assets/file-icons/icons/expui/nodes/class.svg new file mode 100644 index 000000000..77996e71b --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/class.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/class_dark.svg b/linux/assets/file-icons/icons/expui/nodes/class_dark.svg new file mode 100644 index 000000000..a8027940d --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/class_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/enum.svg b/linux/assets/file-icons/icons/expui/nodes/enum.svg new file mode 100644 index 000000000..13deae631 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/enum.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/enum_dark.svg b/linux/assets/file-icons/icons/expui/nodes/enum_dark.svg new file mode 100644 index 000000000..44d6e5cf8 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/enum_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/exception.svg b/linux/assets/file-icons/icons/expui/nodes/exception.svg new file mode 100644 index 000000000..ab5bae754 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/exception.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/exception_dark.svg b/linux/assets/file-icons/icons/expui/nodes/exception_dark.svg new file mode 100644 index 000000000..710a292ec --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/exception_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/folder.svg b/linux/assets/file-icons/icons/expui/nodes/folder.svg new file mode 100644 index 000000000..7ece6b711 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/folder.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/folder_dark.svg b/linux/assets/file-icons/icons/expui/nodes/folder_dark.svg new file mode 100644 index 000000000..0af492bad --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/folder_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/interface.svg b/linux/assets/file-icons/icons/expui/nodes/interface.svg new file mode 100644 index 000000000..b7436907d --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/interface.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/interface_dark.svg b/linux/assets/file-icons/icons/expui/nodes/interface_dark.svg new file mode 100644 index 000000000..53905c525 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/interface_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/package.svg b/linux/assets/file-icons/icons/expui/nodes/package.svg new file mode 100644 index 000000000..4fd24f914 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/package.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/package_dark.svg b/linux/assets/file-icons/icons/expui/nodes/package_dark.svg new file mode 100644 index 000000000..f86ed61ba --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/package_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/record.svg b/linux/assets/file-icons/icons/expui/nodes/record.svg new file mode 100644 index 000000000..bafa1940d --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/record.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/record_dark.svg b/linux/assets/file-icons/icons/expui/nodes/record_dark.svg new file mode 100644 index 000000000..c965cf713 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/record_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/resourcesRoot.svg b/linux/assets/file-icons/icons/expui/nodes/resourcesRoot.svg new file mode 100644 index 000000000..a58f3256d --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/resourcesRoot.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/resourcesRoot_dark.svg b/linux/assets/file-icons/icons/expui/nodes/resourcesRoot_dark.svg new file mode 100644 index 000000000..0a042e94e --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/resourcesRoot_dark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/sourceRoot.svg b/linux/assets/file-icons/icons/expui/nodes/sourceRoot.svg new file mode 100644 index 000000000..4a4b44608 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/sourceRoot.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/sourceRoot_dark.svg b/linux/assets/file-icons/icons/expui/nodes/sourceRoot_dark.svg new file mode 100644 index 000000000..7d3159c25 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/sourceRoot_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/testResourcesRoot.svg b/linux/assets/file-icons/icons/expui/nodes/testResourcesRoot.svg new file mode 100644 index 000000000..1098ebb34 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/testResourcesRoot.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/testResourcesRoot_dark.svg b/linux/assets/file-icons/icons/expui/nodes/testResourcesRoot_dark.svg new file mode 100644 index 000000000..221b77061 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/testResourcesRoot_dark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/testRoot.svg b/linux/assets/file-icons/icons/expui/nodes/testRoot.svg new file mode 100644 index 000000000..4bfdae1a5 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/testRoot.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/expui/nodes/testRoot_dark.svg b/linux/assets/file-icons/icons/expui/nodes/testRoot_dark.svg new file mode 100644 index 000000000..21261bb49 --- /dev/null +++ b/linux/assets/file-icons/icons/expui/nodes/testRoot_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/map.json b/linux/assets/file-icons/map.json new file mode 100644 index 000000000..53ca02dc3 --- /dev/null +++ b/linux/assets/file-icons/map.json @@ -0,0 +1 @@ +{"defaultFile": "idea-text", "defaultFolder": "idea-folder", "defaultFolderOpen": "idea-folder", "iconDefinitions": {"idea-typeScript": "./icons/expui/fileTypes/typeScript_dark.svg", "idea-javaScript": "./icons/expui/fileTypes/javaScript_dark.svg", "idea-json": "./icons/expui/fileTypes/json_dark.svg", "idea-markdown": "./icons/expui/fileTypes/markdown_dark.svg", "idea-java": "./icons/expui/fileTypes/java_dark.svg", "idea-xml": "./icons/expui/fileTypes/xml_dark.svg", "idea-xsd": "./icons/expui/fileTypes/xsd_dark.svg", "idea-wsdl": "./icons/expui/fileTypes/wsdl_dark.svg", "idea-xhtml": "./icons/expui/fileTypes/xhtml_dark.svg", "idea-yaml": "./icons/expui/fileTypes/yaml_dark.svg", "idea-html": "./icons/expui/fileTypes/html_dark.svg", "idea-css": "./icons/expui/fileTypes/css_dark.svg", "idea-sql": "./icons/expui/fileTypes/sql_dark.svg", "idea-shell": "./icons/expui/fileTypes/shell_dark.svg", "idea-powershell": "./icons/expui/fileTypes/powershell_dark.svg", "idea-properties": "./icons/expui/fileTypes/properties_dark.svg", "idea-toml": "./icons/expui/fileTypes/toml_dark.svg", "idea-csv": "./icons/expui/fileTypes/csv_dark.svg", "idea-http": "./icons/expui/fileTypes/http.svg", "idea-image": "./icons/expui/fileTypes/image_dark.svg", "idea-archive": "./icons/expui/fileTypes/archive_dark.svg", "idea-font": "./icons/expui/fileTypes/font_dark.svg", "idea-vue": "./icons/expui/fileTypes/vue.svg", "idea-graphql": "./icons/expui/fileTypes/graphql_dark.svg", "idea-terraform": "./icons/expui/fileTypes/terraform_dark.svg", "idea-swiftLang": "./icons/expui/fileTypes/swiftLang_dark.svg", "idea-c": "./icons/expui/fileTypes/c_dark.svg", "idea-h": "./icons/expui/fileTypes/h_dark.svg", "idea-cpp": "./icons/expui/fileTypes/cpp_dark.svg", "idea-Csharp": "./icons/expui/fileTypes/Csharp_dark.svg", "idea-jsp": "./icons/expui/fileTypes/jsp_dark.svg", "idea-gradle": "./icons/expui/fileTypes/gradle_dark.svg", "idea-groovy": "./icons/expui/fileTypes/groovy_dark.svg", "idea-perl": "./icons/expui/fileTypes/perl_dark.svg", "idea-rst": "./icons/expui/fileTypes/rst_dark.svg", "idea-patch": "./icons/expui/fileTypes/patch_dark.svg", "idea-text": "./icons/expui/fileTypes/text_dark.svg", "idea-class": "./icons/expui/nodes/class_dark.svg", "idea-interface": "./icons/expui/nodes/interface_dark.svg", "idea-enum": "./icons/expui/nodes/enum_dark.svg", "idea-annotation": "./icons/expui/nodes/annotation_dark.svg", "idea-record": "./icons/expui/nodes/record_dark.svg", "idea-exception": "./icons/expui/nodes/exception_dark.svg", "idea-maven": "./icons/expui/fileTypes/maven_dark.svg", "idea-docker": "./icons/expui/fileTypes/docker_dark.svg", "idea-gitignore": "./icons/expui/fileTypes/gitignore.svg", "idea-editorConfig": "./icons/expui/fileTypes/editorConfig_dark.svg", "idea-sourceRoot": "./icons/expui/nodes/sourceRoot_dark.svg", "idea-testRoot": "./icons/expui/nodes/testRoot_dark.svg", "idea-resourcesRoot": "./icons/expui/nodes/resourcesRoot_dark.svg", "idea-testResourcesRoot": "./icons/expui/nodes/testResourcesRoot_dark.svg", "idea-package": "./icons/expui/nodes/package_dark.svg", "idea-folder": "./icons/expui/nodes/folder_dark.svg"}, "lightIconDefinitions": {"idea-typeScript": "./icons/expui/fileTypes/typeScript.svg", "idea-javaScript": "./icons/expui/fileTypes/javaScript.svg", "idea-json": "./icons/expui/fileTypes/json.svg", "idea-markdown": "./icons/expui/fileTypes/markdown.svg", "idea-java": "./icons/expui/fileTypes/java.svg", "idea-xml": "./icons/expui/fileTypes/xml.svg", "idea-xsd": "./icons/expui/fileTypes/xsd.svg", "idea-wsdl": "./icons/expui/fileTypes/wsdl.svg", "idea-xhtml": "./icons/expui/fileTypes/xhtml.svg", "idea-yaml": "./icons/expui/fileTypes/yaml.svg", "idea-html": "./icons/expui/fileTypes/html.svg", "idea-css": "./icons/expui/fileTypes/css.svg", "idea-sql": "./icons/expui/fileTypes/sql.svg", "idea-shell": "./icons/expui/fileTypes/shell.svg", "idea-powershell": "./icons/expui/fileTypes/powershell.svg", "idea-properties": "./icons/expui/fileTypes/properties.svg", "idea-toml": "./icons/expui/fileTypes/toml.svg", "idea-csv": "./icons/expui/fileTypes/csv.svg", "idea-http": "./icons/expui/fileTypes/http.svg", "idea-image": "./icons/expui/fileTypes/image.svg", "idea-archive": "./icons/expui/fileTypes/archive.svg", "idea-font": "./icons/expui/fileTypes/font.svg", "idea-vue": "./icons/expui/fileTypes/vue.svg", "idea-graphql": "./icons/expui/fileTypes/graphql.svg", "idea-terraform": "./icons/expui/fileTypes/terraform.svg", "idea-swiftLang": "./icons/expui/fileTypes/swiftLang.svg", "idea-c": "./icons/expui/fileTypes/c.svg", "idea-h": "./icons/expui/fileTypes/h.svg", "idea-cpp": "./icons/expui/fileTypes/cpp.svg", "idea-Csharp": "./icons/expui/fileTypes/Csharp.svg", "idea-jsp": "./icons/expui/fileTypes/jsp.svg", "idea-gradle": "./icons/expui/fileTypes/gradle.svg", "idea-groovy": "./icons/expui/fileTypes/groovy.svg", "idea-perl": "./icons/expui/fileTypes/perl.svg", "idea-rst": "./icons/expui/fileTypes/rst.svg", "idea-patch": "./icons/expui/fileTypes/patch.svg", "idea-text": "./icons/expui/fileTypes/text.svg", "idea-class": "./icons/expui/nodes/class.svg", "idea-interface": "./icons/expui/nodes/interface.svg", "idea-enum": "./icons/expui/nodes/enum.svg", "idea-annotation": "./icons/expui/nodes/annotation.svg", "idea-record": "./icons/expui/nodes/record.svg", "idea-exception": "./icons/expui/nodes/exception.svg", "idea-maven": "./icons/expui/fileTypes/maven.svg", "idea-docker": "./icons/expui/fileTypes/docker.svg", "idea-gitignore": "./icons/expui/fileTypes/gitignore.svg", "idea-editorConfig": "./icons/expui/fileTypes/editorConfig.svg", "idea-sourceRoot": "./icons/expui/nodes/sourceRoot.svg", "idea-testRoot": "./icons/expui/nodes/testRoot.svg", "idea-resourcesRoot": "./icons/expui/nodes/resourcesRoot.svg", "idea-testResourcesRoot": "./icons/expui/nodes/testResourcesRoot.svg", "idea-package": "./icons/expui/nodes/package.svg", "idea-folder": "./icons/expui/nodes/folder.svg"}, "fileExtensions": {".ts": "idea-typeScript", ".mts": "idea-typeScript", ".cts": "idea-typeScript", ".tsx": "idea-typeScript", ".js": "idea-javaScript", ".mjs": "idea-javaScript", ".cjs": "idea-javaScript", ".jsx": "idea-javaScript", ".json": "idea-json", ".jsonc": "idea-json", ".json5": "idea-json", ".md": "idea-markdown", ".mdx": "idea-markdown", ".java": "idea-java", ".xml": "idea-xml", ".xsd": "idea-xsd", ".wsdl": "idea-wsdl", ".xhtml": "idea-xhtml", ".yml": "idea-yaml", ".yaml": "idea-yaml", ".html": "idea-html", ".htm": "idea-html", ".css": "idea-css", ".scss": "idea-css", ".less": "idea-css", ".sass": "idea-css", ".sql": "idea-sql", ".sh": "idea-shell", ".bash": "idea-shell", ".zsh": "idea-shell", ".ps1": "idea-powershell", ".psm1": "idea-powershell", ".psd1": "idea-powershell", ".ps1xml": "idea-powershell", ".psc1": "idea-powershell", ".pssc": "idea-powershell", ".properties": "idea-properties", ".toml": "idea-toml", ".csv": "idea-csv", ".http": "idea-http", ".png": "idea-image", ".jpg": "idea-image", ".jpeg": "idea-image", ".gif": "idea-image", ".svg": "idea-image", ".webp": "idea-image", ".ico": "idea-image", ".bmp": "idea-image", ".zip": "idea-archive", ".tar": "idea-archive", ".gz": "idea-archive", ".tgz": "idea-archive", ".7z": "idea-archive", ".rar": "idea-archive", ".ttf": "idea-font", ".otf": "idea-font", ".woff": "idea-font", ".woff2": "idea-font", ".vue": "idea-vue", ".graphql": "idea-graphql", ".gql": "idea-graphql", ".tf": "idea-terraform", ".hcl": "idea-terraform", ".swift": "idea-swiftLang", ".c": "idea-c", ".h": "idea-h", ".cpp": "idea-cpp", ".cc": "idea-cpp", ".cxx": "idea-cpp", ".hpp": "idea-h", ".cs": "idea-Csharp", ".jsp": "idea-jsp", ".gradle": "idea-gradle", ".kts": "idea-gradle", ".groovy": "idea-groovy", ".pl": "idea-perl", ".rst": "idea-rst", ".patch": "idea-patch", ".diff": "idea-patch", ".txt": "idea-text", ".log": "idea-text", ".lock": "idea-text"}, "filenames": {"\u0000lithe:java.class": "idea-class", "\u0000lithe:java.interface": "idea-interface", "\u0000lithe:java.enum": "idea-enum", "\u0000lithe:java.annotation": "idea-annotation", "\u0000lithe:java.record": "idea-record", "\u0000lithe:java.exception": "idea-exception", "pom.xml": "idea-maven", "dockerfile": "idea-docker", ".gitignore": "idea-gitignore", ".editorconfig": "idea-editorConfig", ".env": "idea-properties", "license": "idea-text", "notice": "idea-text"}, "folders": {"\u0000lithe:folder.source-root": "idea-sourceRoot", "\u0000lithe:folder.test-root": "idea-testRoot", "\u0000lithe:folder.resources-root": "idea-resourcesRoot", "\u0000lithe:folder.test-resources-root": "idea-testResourcesRoot", "\u0000lithe:folder.package": "idea-package"}, "expandedFolders": {"\u0000lithe:folder.source-root": "idea-sourceRoot", "\u0000lithe:folder.test-root": "idea-testRoot", "\u0000lithe:folder.resources-root": "idea-resourcesRoot", "\u0000lithe:folder.test-resources-root": "idea-testResourcesRoot", "\u0000lithe:folder.package": "idea-package"}} \ No newline at end of file diff --git a/linux/assets/logo.png b/linux/assets/logo.png new file mode 100644 index 000000000..4f3123094 Binary files /dev/null and b/linux/assets/logo.png differ diff --git a/linux/build.rs b/linux/build.rs new file mode 100644 index 000000000..60f3b27db --- /dev/null +++ b/linux/build.rs @@ -0,0 +1,325 @@ +//! 构建脚本:把 `assets/file-icons/map.json` 编译为静态查找表与 SVG 内嵌字节。 +//! +//! 生成物 `$OUT_DIR/file_icons_generated.rs` 提供默认定义 id、各查找表的 +//! `match` 查询函数,以及 `svg_bytes_for_def`(`include_bytes!` 内嵌 SVG)。 +//! 运行期不再解析 JSON;`map.json` 缺失或形状不符时生成空实现(查询一律 +//! 返回 `None`),保证缺资产仍可编译。 + +use std::path::PathBuf; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=assets/file-icons/map.json"); + println!("cargo:rerun-if-changed=assets/file-icons/icons"); + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let out_path = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR 未设置")) + .join("file_icons_generated.rs"); + let code = match std::fs::read_to_string(manifest_dir.join("assets/file-icons/map.json")) { + Ok(json) => generate(&json), + Err(_) => generate(""), + }; + std::fs::write(&out_path, code).expect("写入 file_icons_generated.rs 失败"); +} + +/// 顶层值:字符串或扁平字符串表(`map.json` 的实际形状)。 +enum TopValue { + Str(String), + Map(Vec<(String, String)>), +} + +/// 跳过 JSON 空白。 +fn skip_ws(b: &[u8], pos: &mut usize) { + while *pos < b.len() && matches!(b[*pos], b' ' | b'\t' | b'\n' | b'\r') { + *pos += 1; + } +} + +/// 解析 `"..."` 字符串,处理常见转义(含 `\uXXXX`)。 +fn parse_string(b: &[u8], pos: &mut usize) -> Option { + if b.get(*pos) != Some(&b'"') { + return None; + } + *pos += 1; + let mut out = String::new(); + while let Some(&c) = b.get(*pos) { + *pos += 1; + match c { + b'"' => return Some(out), + b'\\' => { + let e = *b.get(*pos)?; + *pos += 1; + match e { + b'"' => out.push('"'), + b'\\' => out.push('\\'), + b'/' => out.push('/'), + b'b' => out.push('\u{0008}'), + b'f' => out.push('\u{000C}'), + b'n' => out.push('\n'), + b'r' => out.push('\r'), + b't' => out.push('\t'), + b'u' => { + let hex = std::str::from_utf8(b.get(*pos..*pos + 4)?).ok()?; + let cp = u32::from_str_radix(hex, 16).ok()?; + *pos += 4; + out.push(char::from_u32(cp)?); + } + _ => return None, + } + } + _ => { + // 按 UTF-8 边界收一个字符(键与路径多为 ASCII,该分支只做兜底)。 + let s = std::str::from_utf8(&b[*pos - 1..]).ok()?; + let ch = s.chars().next()?; + *pos += ch.len_utf8() - 1; + out.push(ch); + } + } + } + None +} + +/// 跳过任意 JSON 值(对象/数组/字符串/字面量),单个坏条目不污染整表。 +fn skip_value(b: &[u8], pos: &mut usize) -> Option<()> { + skip_ws(b, pos); + match *b.get(*pos)? { + b'"' => { + parse_string(b, pos)?; + } + b'{' | b'[' => { + let mut stack = vec![b[*pos]]; + *pos += 1; + while let Some(&top) = stack.last() { + let c = *b.get(*pos)?; + if c == b'"' { + parse_string(b, pos)?; + continue; + } + *pos += 1; + match c { + b'{' | b'[' => stack.push(c), + b'}' if top == b'{' => { + stack.pop(); + } + b']' if top == b'[' => { + stack.pop(); + } + _ => {} + } + } + } + _ => { + while let Some(&c) = b.get(*pos) { + if matches!(c, b',' | b'}' | b']') || c.is_ascii_whitespace() { + break; + } + *pos += 1; + } + } + } + Some(()) +} + +/// 解析 `{ "k": "v", ... }` 扁平字符串表;非字符串值跳过该条目。 +fn parse_string_map(b: &[u8], pos: &mut usize) -> Option> { + if b.get(*pos) != Some(&b'{') { + return None; + } + *pos += 1; + let mut pairs = Vec::new(); + loop { + skip_ws(b, pos); + if b.get(*pos) == Some(&b'}') { + *pos += 1; + return Some(pairs); + } + let key = parse_string(b, pos)?; + skip_ws(b, pos); + if b.get(*pos) != Some(&b':') { + return None; + } + *pos += 1; + skip_ws(b, pos); + if let Some(value) = parse_string(b, pos) { + pairs.push((key, value)); + } else { + skip_value(b, pos)?; + } + skip_ws(b, pos); + match b.get(*pos) { + Some(b',') => *pos += 1, + Some(b'}') => continue, + _ => return None, + } + } +} + +/// 解析顶层对象,只保留字符串与扁平字符串表,其余跳过;形状不符时返回已收部分。 +fn parse_top(json: &str) -> Vec<(String, TopValue)> { + let b = json.as_bytes(); + let mut pos = 0; + let mut out = Vec::new(); + skip_ws(b, &mut pos); + if b.get(pos) != Some(&b'{') { + return out; + } + pos += 1; + loop { + skip_ws(b, &mut pos); + if b.get(pos) == Some(&b'}') { + return out; + } + let key = match parse_string(b, &mut pos) { + Some(k) => k, + None => return out, + }; + skip_ws(b, &mut pos); + if b.get(pos) != Some(&b':') { + return out; + } + pos += 1; + skip_ws(b, &mut pos); + if b.get(pos) == Some(&b'{') { + match parse_string_map(b, &mut pos) { + Some(map) => out.push((key, TopValue::Map(map))), + None => return out, + } + } else if let Some(s) = parse_string(b, &mut pos) { + out.push((key, TopValue::Str(s))); + } else if skip_value(b, &mut pos).is_none() { + return out; + } + skip_ws(b, &mut pos); + match b.get(pos) { + Some(b',') => pos += 1, + Some(b'}') => continue, + _ => return out, + } + } +} + +/// 去重:后出现的覆盖先出现(与 Windows `normalizeLookupMap` 一致),保留首次出现顺序。 +fn dedupe(pairs: Vec<(String, String)>) -> Vec<(String, String)> { + let mut out: Vec<(String, String)> = Vec::new(); + for (k, v) in pairs { + if let Some(slot) = out.iter_mut().find(|(ek, _)| *ek == k) { + slot.1 = v; + } else { + out.push((k, v)); + } + } + out +} + +/// 生成 `fn name(key: &str) -> Option<&'static str>` 形式的 `match` 查找函数。 +fn emit_lookup_fn(name: &str, pairs: &[(String, String)]) -> String { + let mut s = format!("fn {name}(key: &str) -> Option<&'static str> {{\n match key {{\n"); + for (k, v) in pairs { + s.push_str(&format!(" {k:?} => Some({v:?}),\n")); + } + s.push_str(" _ => None,\n }\n}\n"); + s +} + +/// 生成 `svg_bytes_for_def`:深色取深色 definition,浅色取浅色 definition。 +/// 浅色缺专属路径时的回退由调用方处理,此处只生成精确分支。 +fn emit_svg_fn(dark: &[(String, String)], light: &[(String, String)]) -> String { + let mut s = String::from( + "fn svg_bytes_for_def(def: &str, is_dark: bool) -> Option<&'static [u8]> {\n match (def, is_dark) {\n", + ); + for (id, path) in dark { + s.push_str(&format!( + " ({id:?}, true) => Some(include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/assets/file-icons/\", {path:?})) as &'static [u8]),\n" + )); + } + for (id, path) in light { + s.push_str(&format!( + " ({id:?}, false) => Some(include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/assets/file-icons/\", {path:?})) as &'static [u8]),\n" + )); + } + s.push_str(" _ => None,\n }\n}\n"); + s +} + +/// 生成 `$OUT_DIR/file_icons_generated.rs` 全文;输入为空或形状不符时退化为空实现。 +fn generate(json: &str) -> String { + let top = parse_top(json); + let str_field = |key: &str| -> &str { + top.iter() + .find_map(|(k, v)| match v { + TopValue::Str(s) if k == key => Some(s.as_str()), + _ => None, + }) + .unwrap_or("") + }; + let raw_table = |key: &str| -> Vec<(String, String)> { + top.iter() + .find_map(|(k, v)| match v { + TopValue::Map(m) if k == key => Some(m.clone()), + _ => None, + }) + .unwrap_or_default() + }; + // 键统一转小写(对齐 Windows `normalizeLookupMap`),扩展名保证前导 `.`。 + let lower = |pairs: Vec<(String, String)>| { + dedupe( + pairs + .into_iter() + .map(|(k, v)| (k.to_lowercase(), v)) + .collect(), + ) + }; + let filenames = lower(raw_table("filenames")); + let file_extensions = dedupe( + raw_table("fileExtensions") + .into_iter() + .map(|(k, v)| { + let k = k.to_lowercase(); + let k = if k.starts_with('.') { + k + } else { + format!(".{k}") + }; + (k, v) + }) + .collect(), + ); + let folders = lower(raw_table("folders")); + let expanded_folders = lower(raw_table("expandedFolders")); + // definition 路径去掉 `./` 后拼到 `linux/assets/file-icons/` 即真实文件。 + let strip_dot = |pairs: Vec<(String, String)>| { + dedupe( + pairs + .into_iter() + .map(|(k, v)| { + let v = v.strip_prefix("./").unwrap_or(&v).to_string(); + (k, v) + }) + .collect(), + ) + }; + let dark = strip_dot(raw_table("iconDefinitions")); + // 浅色表与深色表是同一批 id 的两套路径:每个 id 生成 `(id, true)` 与 + // `(id, false)` 两支(元组第二元不同,不冲突);深色独有的 id 浅色分支由调用方回退深色。 + let light = strip_dot(raw_table("lightIconDefinitions")); + + let mut s = + String::from("// 由 linux/build.rs 按 assets/file-icons/map.json 生成,不要手改。\n"); + s.push_str(&format!( + "fn default_file_def() -> &'static str {{ {:?} }}\n", + str_field("defaultFile") + )); + s.push_str(&format!( + "fn default_folder_def() -> &'static str {{ {:?} }}\n", + str_field("defaultFolder") + )); + s.push_str(&format!( + "fn default_folder_open_def() -> &'static str {{ {:?} }}\n", + str_field("defaultFolderOpen") + )); + s.push_str(&emit_lookup_fn("filename_def", &filenames)); + s.push_str(&emit_lookup_fn("file_extension_def", &file_extensions)); + s.push_str(&emit_lookup_fn("folder_def", &folders)); + s.push_str(&emit_lookup_fn("expanded_folder_def", &expanded_folders)); + s.push_str(&emit_svg_fn(&dark, &light)); + s +} diff --git a/linux/src/core/client.rs b/linux/src/core/client.rs new file mode 100644 index 000000000..1577980b2 --- /dev/null +++ b/linux/src/core/client.rs @@ -0,0 +1,272 @@ +use gpui_kit::{BackgroundExecutor, Global, Task}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct CoreRequestEnvelope { + #[serde(skip_serializing_if = "Option::is_none")] + id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + operation_id: Option, + command: String, + payload: Value, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +struct RawCoreResponseEnvelope { + #[allow(dead_code)] + id: Option, + ok: bool, + data: Option, + error: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CoreResponseError { + pub code: Value, + pub message: String, + #[serde(default)] + pub details: Option, +} + +impl std::fmt::Display for CoreResponseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.code { + Value::String(code) => write!(f, "{code}: {}", self.message), + other => write!(f, "{other}: {}", self.message), + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct CoreClient; + +impl Global for CoreClient {} + +impl CoreClient { + pub fn new() -> Self { + Self + } + + /// 执行 Core 命令并返回异步任务 Task + pub fn execute( + &self, + cx: &gpui_kit::AsyncApp, + command: &str, + payload: T, + ) -> Task> + where + T: Serialize, + R: serde::de::DeserializeOwned + Send + 'static, + { + self.execute_with_operation_id(cx, command, payload, None) + } + + /// 执行带 operation_id 的 Core 命令,方便后续取消 + pub fn execute_with_operation_id( + &self, + cx: &gpui_kit::AsyncApp, + command: &str, + payload: T, + operation_id: Option, + ) -> Task> + where + T: Serialize, + R: serde::de::DeserializeOwned + Send + 'static, + { + self.execute_with_executor(cx.background_executor(), command, payload, operation_id) + } + + /// 基于指定 BackgroundExecutor 执行 Core 命令 + pub fn execute_with_executor( + &self, + executor: &BackgroundExecutor, + command: &str, + payload: T, + operation_id: Option, + ) -> Task> + where + T: Serialize, + R: serde::de::DeserializeOwned + Send + 'static, + { + let id = Uuid::new_v4().to_string(); + let payload_value = match serde_json::to_value(payload) { + Ok(v) => v, + Err(e) => { + let err = format!("Failed to serialize command payload: {e}"); + return executor.spawn(async move { Err(err) }); + } + }; + + let request = CoreRequestEnvelope { + id: Some(id), + operation_id, + command: command.to_string(), + payload: payload_value, + }; + + let request_json = match serde_json::to_string(&request) { + Ok(j) => j, + Err(e) => { + let err = format!("Failed to serialize request envelope: {e}"); + return executor.spawn(async move { Err(err) }); + } + }; + + executor.spawn(async move { + let response_json = lithe_core::execute_json(&request_json); + Self::parse_response::(&response_json) + }) + } + + /// 取消正在执行的操作 + #[allow(dead_code)] + pub fn cancel_operation(&self, operation_id: &str) -> bool { + lithe_core::cancel_operation(operation_id) + } + + /// 取消正在执行的操作(别名) + #[allow(dead_code)] + pub fn cancel(&self, operation_id: &str) -> bool { + self.cancel_operation(operation_id) + } + + /// 获取工作区快照 + pub fn snapshot( + &self, + cx: &gpui_kit::AsyncApp, + root: &str, + ) -> Task> { + self.execute( + cx, + "workspace.snapshot", + serde_json::json!({ + "root": root, + }), + ) + } + + /// 读取文件内容 + pub fn read_file( + &self, + cx: &gpui_kit::AsyncApp, + root: &str, + path: &str, + ) -> Task> { + let task: Task> = self.execute( + cx, + "file.read", + serde_json::json!({ + "root": root, + "path": path, + }), + ); + + cx.background_executor().spawn(async move { + let value = task.await?; + let text = value + .get("text") + .and_then(|t| t.as_str()) + .unwrap_or("") + .to_string(); + Ok(text) + }) + } + + /// 写入文件内容 + pub fn write_file( + &self, + cx: &gpui_kit::AsyncApp, + root: &str, + path: &str, + text: &str, + ) -> Task> { + let task: Task> = self.execute( + cx, + "file.write", + serde_json::json!({ + "root": root, + "path": path, + "text": text, + }), + ); + + cx.background_executor().spawn(async move { + task.await?; + Ok(()) + }) + } + + /// 在工作区中搜索文本或文件 + #[allow(dead_code)] + pub fn search( + &self, + cx: &gpui_kit::AsyncApp, + root: &str, + query: &str, + ) -> Task, String>> { + let task: Task> = self.execute( + cx, + "workspace.search", + serde_json::json!({ + "root": root, + "query": query, + "caseSensitive": false, + "maxResults": 100, + }), + ); + + cx.background_executor().spawn(async move { + let value = task.await?; + let matches = value + .get("matches") + .and_then(|m| m.as_array()) + .cloned() + .unwrap_or_default(); + Ok(matches) + }) + } + + /// 获取 Git 状态 + pub fn git_status( + &self, + cx: &gpui_kit::AsyncApp, + root: &str, + ) -> Task> { + self.execute( + cx, + "git.status", + serde_json::json!({ + "root": root, + }), + ) + } + + /// 解析 Core 返回的 JSON envelope + fn parse_response(response_json: &str) -> Result { + let envelope: RawCoreResponseEnvelope = serde_json::from_str(response_json) + .map_err(|e| format!("Failed to parse core response JSON: {e}"))?; + + if envelope.ok { + let data = envelope.data.unwrap_or(Value::Null); + serde_json::from_value::(data) + .map_err(|e| format!("Failed to deserialize response data: {e}")) + } else { + let error_str = if let Some(err) = envelope.error { + match &err.details { + Some(details) if !details.is_empty() => { + format!("{err} ({details})") + } + _ => err.to_string(), + } + } else { + "Unknown error from lithe core".to_string() + }; + Err(error_str) + } + } +} diff --git a/linux/src/core/mod.rs b/linux/src/core/mod.rs new file mode 100644 index 000000000..f2702b963 --- /dev/null +++ b/linux/src/core/mod.rs @@ -0,0 +1,3 @@ +mod client; + +pub use client::CoreClient; diff --git a/linux/src/i18n.rs b/linux/src/i18n.rs new file mode 100644 index 000000000..38b478628 --- /dev/null +++ b/linux/src/i18n.rs @@ -0,0 +1,752 @@ +//! 菜单文案国际化:键与 Tauri `menu.*` / `titleProject.*` 对齐,按设置取中英文。 +//! +//! 真源对应关系: +//! - 键名与 [`menu_text`] 的中英文逐条抄自 +//! `windows/tauri/src/i18n/locale.ts`(英文 `menu.*`、中文 `menu.*`、 +//! `titleProject.*`),不臆造;键不存在返回 `""`。 +//! - 中英文切换依据 `display_language`(`linux/src/settings.rs`), +//! 以 `zh` 开头取中文,否则取英文,与 Tauri 按语言取文案一致。 + +use gpui_kit::App; + +use crate::settings; + +/// 当前是否为中文(`display_language` 以 `zh` 开头即中文)。 +pub fn is_zh(cx: &App) -> bool { + settings::get(cx).display_language.starts_with("zh") +} + +/// 取菜单文案:键与 Tauri `menu.*` / `titleProject.*` 对齐,不存在返回 `""`。 +pub fn menu_text(cx: &App, key: &str) -> &'static str { + let (zh, en) = match key { + // ---- 顶层菜单标题 ---- + "menu.file" => ("文件", "File"), + "menu.edit" => ("编辑", "Edit"), + "menu.view" => ("视图", "View"), + "menu.go" => ("转到", "Go"), + "menu.terminal" => ("终端", "Terminal"), + "menu.run" => ("运行", "Run"), + "menu.tools" => ("工具", "Tools"), + "menu.window" => ("窗口", "Window"), + "menu.help" => ("帮助", "Help"), + "menu.theme" => ("主题", "Theme"), + // ---- File ---- + "menu.newTab" => ("新建标签页", "New Tab"), + "menu.newWindow" => ("新建窗口", "New Window"), + "menu.newFile" => ("新建文件", "New File"), + "menu.openFolder" => ("打开文件夹", "Open Folder"), + "menu.closeFolder" => ("关闭文件夹", "Close Folder"), + "menu.save" => ("保存", "Save"), + "menu.saveAs" => ("另存为...", "Save As..."), + "menu.saveAll" => ("全部保存", "Save All"), + "menu.revertFile" => ("还原文件", "Revert File"), + "menu.showLocalHistory" => ("显示本地历史", "Show Local History"), + "menu.closeTab" => ("关闭标签页", "Close Tab"), + "menu.closeWindow" => ("关闭窗口", "Close Window"), + "menu.closeAllTabs" => ("关闭所有标签页", "Close All Tabs"), + "menu.closeOtherTabs" => ("关闭其他标签页", "Close Other Tabs"), + "menu.closeSavedTabs" => ("关闭已保存标签页", "Close Saved Tabs"), + "menu.closeTabsToLeft" => ("关闭左侧标签页", "Close Tabs to the Left"), + "menu.closeTabsToRight" => ("关闭右侧标签页", "Close Tabs to the Right"), + "menu.reopenClosedTab" => ("重新打开已关闭标签页", "Reopen Closed Tab"), + "menu.quit" => ("退出", "Quit"), + // ---- Edit ---- + "menu.undo" => ("撤销", "Undo"), + "menu.redo" => ("重做", "Redo"), + "menu.cut" => ("剪切", "Cut"), + "menu.copy" => ("复制", "Copy"), + "menu.paste" => ("粘贴", "Paste"), + "menu.selectAll" => ("全选", "Select All"), + "menu.find" => ("查找", "Find"), + "menu.findAndReplace" => ("查找并替换", "Find and Replace"), + "menu.toggleComment" => ("切换注释", "Toggle Comment"), + "menu.quickFix" => ("快速修复...", "Quick Fix"), + "menu.triggerParameterHints" => ("触发参数提示", "Trigger Parameter Hints"), + "menu.showHover" => ("显示悬停信息", "Show Hover"), + "menu.duplicateLine" => ("复制行", "Duplicate Line"), + "menu.deleteLine" => ("删除行", "Delete Line"), + "menu.moveLineUp" => ("上移行", "Move Line Up"), + "menu.moveLineDown" => ("下移行", "Move Line Down"), + "menu.formatDocument" => ("格式化文档", "Format Document"), + "menu.formatSelection" => ("格式化所选内容", "Format Selection"), + "menu.commandPalette" => ("命令面板", "Command Palette"), + // ---- View ---- + "menu.toggleActivitySidebar" => ("切换活动侧栏", "Toggle Activity Sidebar"), + "menu.toggleSecondarySidebar" => ("切换辅助侧栏", "Toggle Secondary Sidebar"), + "menu.toggleTerminal" => ("切换终端", "Toggle Terminal"), + "menu.globalSearch" => ("全局搜索", "Global Search"), + "menu.diagnostics" => ("诊断", "Diagnostics"), + "menu.fileExplorer" => ("文件资源管理器", "File Explorer"), + "menu.sourceControl" => ("源代码管理", "Source Control"), + "menu.github" => ("GitHub", "GitHub"), + "menu.runAndDebug" => ("运行和调试", "Run and Debug"), + "menu.splitEditor" => ("拆分编辑器", "Split Editor"), + "menu.toggleMinimap" => ("切换缩略图", "Toggle Minimap"), + "menu.toggleWordWrap" => ("切换自动换行", "Toggle Word Wrap"), + "menu.toggleLineNumbers" => ("切换行号", "Toggle Line Numbers"), + "menu.toggleRenderWhitespace" => ("切换空白字符显示", "Toggle Render Whitespace"), + "menu.zoomIn" => ("放大", "Zoom In"), + "menu.zoomOut" => ("缩小", "Zoom Out"), + "menu.resetZoom" => ("重置缩放", "Reset Zoom"), + // ---- Go ---- + "menu.quickOpen" => ("快速打开", "Quick Open"), + "menu.goToLine" => ("转到行", "Go to Line"), + "menu.goBack" => ("后退", "Go Back"), + "menu.goForward" => ("前进", "Go Forward"), + "menu.goToDefinition" => ("转到定义", "Go to Definition"), + "menu.goToImplementation" => ("转到实现", "Go to Implementation"), + "menu.goToTypeDefinition" => ("转到类型定义", "Go to Type Definition"), + "menu.goToReferences" => ("转到引用", "Go to References"), + "menu.renameSymbol" => ("重命名符号", "Rename Symbol"), + "menu.nextTab" => ("下一个标签页", "Next Tab"), + "menu.previousTab" => ("上一个标签页", "Previous Tab"), + // ---- Terminal ---- + "menu.newTerminal" => ("新建终端", "New Terminal"), + "menu.splitTerminalRight" => ("向右拆分终端", "Split Terminal Right"), + "menu.splitTerminalDown" => ("向下拆分终端", "Split Terminal Down"), + "menu.closeTerminal" => ("关闭终端", "Close Terminal"), + // ---- Run ---- + "menu.startDebugging" => ("开始调试", "Start Debugging"), + "menu.stopDebugging" => ("停止调试", "Stop Debugging"), + "menu.toggleBreakpoint" => ("切换断点", "Toggle Breakpoint"), + // ---- Tools ---- + "menu.databases" => ("数据库", "Databases"), + "menu.webInspector" => ("Web 检查器", "Web Inspector"), + "menu.preferences" => ("首选项", "Preferences"), + "menu.keyboardShortcuts" => ("键盘快捷键", "Keyboard Shortcuts"), + // ---- Window ---- + "menu.minimize" => ("最小化", "Minimize"), + "menu.maximize" => ("最大化", "Maximize"), + "menu.toggleFullscreen" => ("切换全屏", "Toggle Fullscreen"), + // ---- Help ---- + "menu.documentation" => ("文档", "Documentation"), + "menu.whatsNew" => ("新增功能", "What's New"), + "menu.changelog" => ("更新日志", "Changelog"), + "menu.reportBug" => ("报告 Bug", "Report a Bug"), + "menu.requestFeature" => ("请求新功能", "Request a Feature"), + "menu.checkForUpdates" => ("检查更新", "Check for Updates"), + // ---- 项目胶囊(titleProject.*) ---- + "titleProject.newProject" => ("新建项目…", "New Project…"), + "titleProject.open" => ("打开…", "Open…"), + "titleProject.cloneRepository" => ("克隆仓库…", "Clone Repository…"), + "titleProject.openProjects" => ("打开的项目", "Open Projects"), + "titleProject.recentProjects" => ("最近项目", "Recent Projects"), + "titleProject.noRecentProjects" => ("没有最近项目", "No recent projects"), + // ---- 侧边栏 explorer 头部 ---- + "workbench.project" => ("项目", "Project"), + "workbench.changes" => ("更改", "Changes"), + "workbench.search" => ("搜索", "Search"), + "workbench.run" => ("运行", "Run"), + "run.title" => ("运行", "Run"), + "run.running" => ("运行中", "Running"), + "run.run" => ("运行配置", "Run configuration"), + "run.stop" => ("停止运行", "Stop run"), + "run.rescan" => ("重新扫描服务", "Rescan services"), + "run.clearOutput" => ("清除运行输出", "Clear run output"), + "run.minimize" => ("最小化", "Minimize"), + "run.emptyOutput" => ( + "运行配置后将在这里显示进程输出。", + "Run a configuration to see process output.", + ), + "run.starting" => ("开始运行", "Starting"), + "run.noSteps" => ("无可执行步骤", "No executable steps"), + "run.startFailed" => ("启动失败", "Failed to start"), + "run.exitCode" => ("退出码", "Exit code"), + "run.exited" => ("进程已退出", "Process exited"), + "run.finished" => ("运行已结束", "Run finished"), + "run.stopping" => ("已发送停止信号…", "Stop signal sent…"), + "maven.clearOutput" => ("清除构建输出", "Clear build output"), + "maven.rerunTest" => ("重新运行测试", "Rerun test"), + "workbench.terminal" => ("终端", "Terminal"), + "workbench.diagnostics" => ("诊断", "Diagnostics"), + "workbench.gitLog" => ("提交记录", "Git Log"), + "terminal.session" => ( + "Lithe 终端(PTY 会话:{shell})", + "Lithe Terminal (PTY Session: {shell})", + ), + "terminal.cleared" => ("终端已清空。", "Terminal cleared."), + "terminal.unavailable" => ("PTY 不可用", "PTY unavailable"), + "workbench.settings" => ("设置", "Settings"), + "workbench.sourceControl" => ("源代码管理", "Source Control"), + "fileExplorer.searchFiles" => ("搜索文件", "Search files"), + "fileExplorer.preferences" => ("文件资源管理器偏好设置", "File explorer preferences"), + "fileExplorer.visibility" => ("可见性", "Visibility"), + "settings.tabs.appearance" => ("外观", "Appearance"), + "fileExplorer.fileIcons" => ("文件图标", "File Icons"), + "fileExplorer.indentGuides" => ("缩进参考线", "Indent Guides"), + "fileExplorer.indentation" => ("缩进", "Indentation"), + "fileExplorer.indentationCompact" => ("紧凑", "Compact"), + "fileExplorer.indentationDefault" => ("默认", "Default"), + "fileExplorer.indentationSpacious" => ("宽松", "Spacious"), + "fileExplorer.indentationWide" => ("宽", "Wide"), + "fileExplorer.gitignoredFiles" => ("被 Git 忽略的文件", "Gitignored Files"), + "fileExplorer.gitStatusDecorations" => ("Git 状态标记", "Git Status Decorations"), + "settings.files.sortOrder" => ("排序顺序", "Sort Order"), + "settings.files.foldersFirst" => ("文件夹优先", "Folders First"), + "settings.files.name" => ("名称", "Name"), + "settings.files.compactFolders" => ("紧凑文件夹", "Compact Folders"), + "settings.files.hideRootFolder" => ("隐藏根文件夹", "Hide Root Folder"), + "settings.files.hiddenFiles" => ("隐藏文件", "Hidden Files"), + "settings.files.autoReveal" => ("自动显示活动文件", "Auto Reveal Active File"), + "settings.files.confirmBeforeDelete" => ("删除前确认", "Confirm Before Delete"), + "search.search" => ("搜索", "Search"), + "search.clear" => ("清除搜索", "Clear search"), + "ui.loading" => ("正在加载", "Loading"), + "workbench.searchInFiles" => ("在文件中搜索...", "Search in files..."), + "git.commitMessagePlaceholder" => ("提交说明...", "Commit message..."), + // ---- Maven 面板 ---- + "maven.title" => ("Maven", "Maven"), + "maven.lifecycle" => ("生命周期", "Lifecycle"), + "maven.reloadProjects" => ("重新加载 Maven 项目", "Reload Maven projects"), + "maven.noProjects" => ("无 Maven 项目", "No Maven projects"), + "maven.runSelected" => ("运行选中的生命周期阶段", "Run selected lifecycle phase"), + "maven.stop" => ("停止 Maven 任务", "Stop Maven task"), + "maven.executeGoal" => ("执行 Maven Goal", "Execute Maven goal"), + "maven.skipTests" => ("跳过测试", "Skip tests"), + "maven.collapseAll" => ("全部折叠", "Collapse all"), + "maven.settings" => ("Maven 设置", "Maven Settings"), + "maven.profiles" => ("Profiles", "Profiles"), + "maven.dependencies" => ("依赖项", "Dependencies"), + "maven.sourceRoots" => ("源码根目录", "Source Roots"), + "maven.dependencyLoading" => ("正在解析依赖项...", "Resolving dependencies..."), + "maven.dependencyCancelled" => ("依赖项解析已取消", "Dependency resolution cancelled"), + "maven.noDependencies" => ("没有依赖项", "No dependencies"), + "maven.omittedConflict" => ("版本冲突,采用", "conflict with"), + "maven.omittedDuplicate" => ("已省略重复依赖", "duplicate omitted"), + "maven.project" => ("项目", "Project"), + "maven.loadFailed" => ("无法加载 Maven 项目", "Unable to load Maven project"), + "maven.scanning" => ("正在扫描 Maven 项目...", "Scanning Maven project..."), + "maven.notDetected" => ("未检测到 Maven 项目", "No Maven project detected"), + "ui.retry" => ("重试", "Retry"), + "ui.cancel" => ("取消", "Cancel"), + // ---- 通用 UI ---- + "ui.close" => ("关闭", "Close"), + "ui.refresh" => ("刷新", "Refresh"), + "ui.clear" => ("清空", "Clear"), + // ---- 空态 / 面板标题 ---- + "diagnostics.empty" => ("未检测到问题", "No problems detected"), + "extensions.title" => ("扩展", "Extensions"), + "extensions.all" => ("全部", "All"), + "extensions.languages" => ("语言", "Languages"), + "extensions.themes" => ("主题", "Themes"), + "extensions.iconThemes" => ("图标主题", "Icon Themes"), + "extensions.databases" => ("数据库", "Databases"), + "extensions.ai" => ("AI", "AI"), + "extensions.integrations" => ("集成", "Integrations"), + "extensions.skills" => ("技能", "Skills"), + "extensions.agents" => ("代理", "Agents"), + "extensions.searchPlaceholder" => ("搜索扩展", "Search extensions"), + "extensions.availableCount" => ("{count} 个可用", "{count} available"), + "extensions.installedCount" => ("{count} 个已安装", "{count} installed"), + "extensions.noneFound" => ("未找到扩展。", "No extensions found."), + "extensions.noneSelected" => ("未选择扩展。", "No extension selected."), + "extensions.byPublisher" => ("作者:{publisher}", "By {publisher}"), + "extensions.contributions" => ("贡献项", "Contributions"), + "extensions.installed" => ("已安装", "Installed"), + "extensions.disabled" => ("已禁用", "Disabled"), + "extensions.builtIn" => ("内置", "Built-in"), + "extensions.activate" => ("启用", "Activate"), + "extensions.deactivate" => ("停用", "Deactivate"), + "notifications.title" => ("通知", "Notifications"), + "notifications.filter" => ("筛选通知", "Filter Notifications"), + "notifications.empty" => ("暂无通知。", "No notifications yet."), + "notifications.noMatch" => ("没有匹配的通知。", "No matching notifications."), + "notifications.markAllRead" => ("全部标为已读", "Mark all read"), + "notifications.delete" => ("删除", "Delete"), + "notifications.clearAll" => ("全部清除", "Clear All"), + "notifications.details" => ("通知详情", "Notification details"), + "notifications.filterAll" => ("全部", "All"), + "notifications.filterInfo" => ("信息", "Info"), + "notifications.filterSuccess" => ("成功", "Success"), + "notifications.filterWarnings" => ("警告", "Warnings"), + "notifications.filterErrors" => ("错误", "Errors"), + "notifications.projectOpened" => ("已打开项目", "Project opened"), + // ---- Git ---- + "git.commit" => ("提交", "Commit"), + // ---- Git 分支管理器(与 Tauri locale.ts git.* 逐字对齐) ---- + "git.repositories" => ("仓库", "Repositories"), + "git.branches" => ("分支", "Branches"), + "git.worktrees" => ("工作树", "Worktrees"), + "git.searchBranches" => ("搜索分支...", "Search branches..."), + "git.searchWorktrees" => ("搜索工作树...", "Search worktrees..."), + "git.filterRepositories" => ("筛选仓库...", "Filter repositories..."), + "git.branchCount" => ("{count} 个分支", "{count} branch"), + "git.branchesCount" => ("{count} 个分支", "{count} branches"), + "git.worktreeCount" => ("{count} 个工作树", "{count} worktree"), + "git.worktreesCount" => ("{count} 个工作树", "{count} worktrees"), + "git.repositoryCount" => ("{count} 个仓库", "{count} repository"), + "git.repositoriesCount" => ("{count} 个仓库", "{count} repositories"), + "git.selectorSections" => ("Git 选择分区", "Git selector sections"), + "git.noMatchingBranches" => ("没有匹配的分支", "No matching branches"), + "git.noBranchesFound" => ("未找到分支", "No branches found"), + "git.createNewBranch" => ("创建新分支 “{name}”", "Create new branch \"{name}\""), + // ---- Quick Open / Command Palette / Search Everywhere(与 Tauri locale.ts 对齐) ---- + "quickOpen.searchFiles" => ("输入以搜索文件...", "Type to search files..."), + "quickOpen.searchSymbols" => ("输入以筛选符号...", "Type to filter symbols..."), + "quickOpen.searchWorkspaceSymbols" => ( + "在项目中搜索符号...", + "Type to search symbols across the project...", + ), + "quickOpen.noMatch" => ("未找到匹配的文件", "No matching files found"), + "quickOpen.noSymbols" => ("未找到符号", "No symbols found"), + "quickOpen.searching" => ("正在搜索...", "Searching..."), + "quickOpen.filesCount" => ("{count} 个文件", "{count} files"), + "quickOpen.fileCountOne" => ("1 个文件", "1 file"), + "commandPalette.placeholder" => ("输入命令...", "Type a command..."), + "search.noResults" => ("未找到结果", "No results found"), + // ---- 标签页右键菜单 / 文件上下文菜单(与 Tauri locale.ts tabs.* / files.* 对齐) ---- + "tabs.close" => ("关闭", "Close"), + "tabs.closeOthers" => ("关闭其他", "Close Others"), + "tabs.closeToRight" => ("关闭右侧", "Close to Right"), + "tabs.closeAll" => ("全部关闭", "Close All"), + "tabs.reload" => ("重新加载", "Reload"), + "tabs.pin" => ("固定标签页", "Pin Tab"), + "tabs.unpin" => ("取消固定标签页", "Unpin Tab"), + "tabs.splitRight" => ("向右拆分", "Split Right"), + "tabs.splitDown" => ("向下拆分", "Split Down"), + "tabs.lockEditorGroup" => ("锁定编辑器组", "Lock Editor Group"), + "tabs.unlockEditorGroup" => ("解锁编辑器组", "Unlock Editor Group"), + "tabs.cannotClosePinned" => ("无法关闭已固定标签页 {name}", "Cannot close pinned tab {name}"), + "files.copyPath" => ("复制路径", "Copy Path"), + "files.copyRelativePath" => ("复制相对路径", "Copy Relative Path"), + "files.reveal" => ("在资源管理器中显示", "Reveal in File Explorer"), + "files.openInTerminal" => ("在终端中打开", "Open in Terminal"), + // ---- Welcome 屏(与 Tauri locale.ts welcome.* 对齐) ---- + "welcome.searchProjects" => ("搜索项目", "Search projects"), + "welcome.recentProjects" => ("最近项目", "Recent Projects"), + "welcome.openFolderHint" => ("打开文件夹以开始使用。", "Open a folder to get started."), + "welcome.title" => ("欢迎使用 Lithe", "Welcome to Lithe"), + "welcome.projects" => ("项目", "Projects"), + "welcome.open" => ("打开", "Open"), + "welcome.clone" => ("克隆", "Clone"), + "welcome.noRecentProjects" => ("暂无最近项目", "No recent projects"), + "welcome.removeRecent" => ( + "从最近项目中移除 {name}", + "Remove {name} from recent projects", + ), + // ---- Linux-frame 新增键(Tauri 无对应) ---- + "goToLine.title" => ("转到行", "Go to Line"), + "goToLine.placeholder" => ("输入行号...", "Type a line number..."), + "goToLine.hint" => ("按 Enter 跳转到指定行", "Press Enter to go to the specified line"), + "search.scopeAll" => ("全部", "All"), + "search.scopeFiles" => ("文件", "Files"), + "search.scopeActions" => ("操作", "Actions"), + "search.everywherePlaceholder" => ( + "搜索文件、操作和符号...", + "Search files, actions and symbols...", + ), + // ---- 设置对话框分类 / 头部 / 底部(与 Tauri settings-dialog.tsx labelKey 对齐) ---- + "settings.tabs.general" => ("常规", "General"), + "settings.project.title" => ("项目 · JDK 与 Maven", "Project · JDK & Maven"), + "settings.run.title" => ("运行配置", "Run configurations"), + "settings.tabs.editor" => ("编辑器", "Editor"), + "settings.tabs.keyboard" => ("快捷键", "Keybindings"), + "settings.tabs.terminal" => ("终端", "Terminal"), + "settings.tabs.lsp" => ("LSP", "LSP"), + "settings.tabs.ai" => ("AI 聊天与编辑", "AI Chat & Edit"), + "settings.tabs.aiCommit" => ("AI 与提交", "AI & Commit"), + "settings.tabs.git" => ("Git", "Git"), + "settings.tabs.logs" => ("日志", "Logs"), + "settings.tabs.updates" => ("更新", "Updates"), + "settings.mac.restoreDefaults" => ("恢复默认设置", "Restore Defaults"), + "settings.mac.done" => ("完成", "Done"), + "settings.search" => ("搜索设置...", "Search settings..."), + "ui.save" => ("保存", "Save"), + // ---- 设置对话框内容:常规(与 Tauri macos-settings-panels.tsx 对齐) ---- + "settings.mac.appearance" => ("外观", "Appearance"), + "settings.mac.appearanceMode" => ("外观模式", "Appearance mode"), + "settings.mac.appearanceDescription" => ( + "选择界面外观,可固定为浅色或深色,或跟随系统。", + "Choose the interface appearance: light, dark, or follow the system.", + ), + "settings.mac.followSystem" => ("跟随系统", "Follow System"), + "settings.mac.light" => ("浅色", "Light"), + "settings.mac.dark" => ("深色", "Dark"), + "settings.mac.language" => ("语言", "Language"), + "settings.mac.languageDescription" => ( + "界面语言会立即生效。默认语言为英文。", + "The interface language changes immediately. English is the default.", + ), + "settings.mac.projects" => ("项目", "Projects"), + "settings.mac.openProjectsIn" => ("项目打开方式", "Open projects in"), + "settings.mac.openProjectsDescription" => ( + "选择打开其他项目时是每次询问、保留在此窗口,还是创建新窗口。", + "Choose whether opening another project asks first, stays in this window, or creates a new window.", + ), + "settings.mac.askEveryTime" => ("每次询问", "Ask Every Time"), + "settings.mac.thisWindow" => ("此窗口", "This Window"), + "settings.mac.newWindow" => ("新窗口", "New Window"), + "settings.mac.files" => ("文件", "Files"), + "settings.mac.autoSave" => ("自动保存更改的文件", "Save changed files automatically"), + "settings.mac.saveLocalChangesWith" => ("保存本地更改的方式", "Save local changes with"), + "settings.mac.gitPolicyDescription" => ( + "选择执行 Git 操作前保护本地更改的方式。", + "Choose how local changes are protected before a Git operation.", + ), + "settings.mac.git" => ("Git", "Git"), + "settings.mac.shelf" => ("暂存架", "Shelf"), + "settings.mac.gitStash" => ("Git 贮藏", "Git stash"), + "settings.mac.hiddenPaths" => ("隐藏路径", "Hidden paths"), + "settings.mac.hiddenPathsDescription" => ( + "每行一项。目录名称会隐藏匹配的文件夹;文件条目支持 * 和 ?。", + "One entry per line. Directory names hide matching folders; file entries support * and ?.", + ), + "settings.mac.directories" => ("目录", "Directories"), + "settings.mac.filePatterns" => ("文件模式", "File patterns"), + "settings.mac.apply" => ("应用", "Apply"), + // ---- 设置对话框内容:项目 / 运行 ---- + "settings.project.scope" => ( + "仅保存在当前电脑,作用于当前项目。运行配置默认继承这些值,单独设置的覆盖值保持不变。路径留空时使用自动选择。", + "Defaults for this project on this computer. Run configurations inherit these values unless explicitly overridden. Leave a path empty to use automatic selection.", + ), + "settings.run.description" => ( + "选择服务或任务,配置启动参数、环境变量和项目环境覆盖项;点击保存后生效。", + "Select a service or task to configure its arguments, environment and project-environment overrides. Changes apply after Save.", + ), + "run.jdkHome" => ("JDK 主目录", "JDK Home"), + "run.mavenExecutable" => ("Maven 主目录 / 可执行文件", "Maven home or executable"), + "run.mavenJdkHome" => ("Maven JDK 主目录", "Maven JDK Home"), + "run.toolchainAuto" => ("自动检测(留空)", "Auto-detect (leave empty)"), + "settings.project.useProjectJdk" => ("使用项目 JDK", "Use project JDK"), + // ---- 设置对话框内容:编辑器 / 快捷键 / 终端 / LSP ---- + "settings.mac.display" => ("显示", "Display"), + "settings.mac.fontSize" => ("字体大小", "Font size"), + "settings.mac.showCodeVision" => ("显示用法与 Git 作者", "Show usages and Git author"), + "settings.mac.editorTabs" => ("编辑器标签页", "Editor tabs"), + "settings.mac.indentation" => ("缩进", "Indentation"), + "settings.mac.tabWidth" => ("制表符宽度", "Tab width"), + "settings.mac.spaces" => ("个空格", "spaces"), + "settings.editor.bufferCarousel" => ("缓冲区轮播", "Buffer Carousel"), + "settings.editor.bufferCarouselDescription" => ( + "在主视图中将打开的缓冲区显示为可横向滚动的轮播", + "Show open buffers as a horizontally scrollable carousel in the main view", + ), + "settings.editor.fontFamily" => ("编辑器字体", "Editor Font Family"), + "settings.editor.lineHeight" => ("行高", "Line Height"), + "settings.editor.renderWhitespace" => ("显示空白字符", "Render Whitespace"), + "settings.editor.whitespaceNone" => ("无", "None"), + "settings.editor.whitespaceBoundary" => ("边界", "Boundary"), + "settings.editor.whitespaceTrailing" => ("行尾", "Trailing"), + "settings.editor.whitespaceAll" => ("全部", "All"), + "settings.editor.vimMode" => ("Vim 模式", "Vim Mode"), + "settings.editor.formatOnSave" => ("保存时格式化", "Format on Save"), + "settings.editor.wordWrap" => ("自动换行", "Word Wrap"), + "settings.editor.behavior" => ("行为", "Behavior"), + "settings.mac.keymapPreset" => ("快捷键方案", "Keymap"), + "settings.mac.preset" => ("预设", "Preset"), + "settings.mac.shortcuts" => ("键盘快捷键", "Keyboard shortcuts"), + "settings.mac.searchShortcuts" => ("搜索快捷键", "Search shortcuts"), + "settings.mac.shortcutsDescription" => ( + "选择快捷键预设,然后使用命令面板查看和运行可用命令。", + "Select a keymap preset, then use the command palette to inspect and run available commands.", + ), + "settings.mac.shell" => ("Shell", "Shell"), + "settings.mac.defaultShell" => ("默认 Shell", "Default shell"), + "settings.mac.defaultShellDescription" => ( + "用于新的终端会话。", + "Used for new terminal sessions.", + ), + "settings.mac.systemDefault" => ("系统默认", "System Default"), + "settings.mac.shellPowerShell" => ("PowerShell", "PowerShell"), + "settings.mac.shellCommandPrompt" => ("命令提示符", "Command Prompt"), + "settings.mac.shellWsl" => ("WSL", "WSL"), + "settings.terminal.fontSize" => ("字体大小", "Font Size"), + "settings.terminal.cursorBlink" => ("光标闪烁", "Blinking Cursor"), + "settings.terminal.scrollback" => ("回滚缓冲区", "Scrollback"), + "settings.mac.languageServices" => ("语言服务", "Language services"), + "settings.mac.autoCompletion" => ("自动补全", "Auto completion"), + "settings.mac.autoCompletionDescription" => ( + "显示活动语言服务器提供的补全建议。", + "Show completion suggestions from the active language server.", + ), + "settings.mac.parameterHints" => ("参数提示", "Parameter hints"), + "settings.mac.semanticHighlighting" => ("语义高亮", "Semantic highlighting"), + "settings.mac.detectedServers" => ("已检测语言服务器", "Detected language servers"), + "settings.mac.detectedServersDescription" => ( + "语言服务器由已安装的语言扩展检测,并在打开受支持文件时启动。", + "Language servers are detected from installed language extensions and start when a supported file opens.", + ), + // ---- 设置对话框内容:AI(与 Tauri ai-settings.tsx 对齐) ---- + "aiSettings.provider" => ("提供商", "Provider"), + "aiSettings.providerDescription" => ( + "选择 Lithe Agent 使用的提供商", + "Choose the provider used by Lithe Agent", + ), + "aiSettings.model" => ("模型", "Model"), + "aiSettings.modelDescription" => ( + "选择 Lithe Agent 使用的模型", + "Choose the model used by Lithe Agent", + ), + "aiSettings.completion" => ("AI 自动补全", "AI Autocomplete"), + "settings.mac.commitMessage" => ("提交信息", "Commit messages"), + // ---- 设置对话框内容:Git(与 Tauri tabs/git-settings.tsx 对齐) ---- + "settings.git.integration" => ("集成", "Integration"), + "settings.git.gitIntegration" => ("Git 集成", "Git Integration"), + "settings.git.gitIntegrationDescription" => ( + "启用 Git 仓库的源代码管理功能", + "Enable source control management with Git repositories", + ), + "settings.git.autoRefresh" => ("自动刷新 Git 状态", "Auto Refresh Git Status"), + "settings.git.autoRefreshDescription" => ( + "相关文件或 Git 事件发生变化后自动刷新 Git 视图", + "Refresh the Git view automatically after relevant file changes and Git events", + ), + "settings.git.confirmDiscard" => ("丢弃前确认", "Confirm Before Discard"), + "settings.git.confirmDiscardDescription" => ( + "丢弃文件或仓库更改前显示确认提示", + "Show a confirmation before discarding file or repository changes", + ), + "settings.git.view" => ("Git 视图", "Git View"), + "settings.git.folderChanges" => ("基于文件夹的更改", "Folder-Based Changes"), + "settings.git.folderChangesDescription" => ( + "以类似文件视图的文件夹树形式显示 Git 更改", + "Show Git changes in a folder tree, similar to Files", + ), + "settings.git.untracked" => ("显示未跟踪文件", "Show Untracked Files"), + "settings.git.untrackedDescription" => ( + "在 Git 状态面板中显示未跟踪文件", + "Display untracked files in the Git status panel", + ), + "settings.git.stagedFirst" => ("优先显示已暂存项", "Show Staged First"), + "settings.git.stagedFirstDescription" => ( + "在 Git 面板中将已暂存更改显示在未暂存更改之前", + "Render staged changes above unstaged changes in the Git panel", + ), + "settings.git.openDiff" => ("单击打开差异", "Open Diff On Click"), + "settings.git.openDiffDescription" => ( + "单击已更改文件时打开差异,而不是直接打开文件", + "Open the diff when clicking a changed file instead of opening the file directly", + ), + "settings.git.compactBadges" => ("紧凑 Git 状态标记", "Compact Git Status Badges"), + "settings.git.compactBadgesDescription" => ( + "在 Git 面板中使用更紧凑的差异统计和暂存标签布局", + "Use a denser layout for diff stats and staged labels in the Git panel", + ), + "settings.git.collapseEmpty" => ("折叠空分区", "Collapse Empty Sections"), + "settings.git.collapseEmptyDescription" => ( + "没有项目时隐藏已暂存更改等空 Git 分区", + "Hide empty Git sections like Staged Changes when they have no items", + ), + "settings.git.rememberPanel" => ("记住上次 Git 面板模式", "Remember Last Git Panel Mode"), + "settings.git.rememberPanelDescription" => ( + "重新打开 Git 视图时恢复上次打开的底部 Git 面板分区", + "Restore the last open bottom Git panel section when reopening the Git view", + ), + "settings.git.defaultDiff" => ("默认差异视图", "Default Diff View"), + "settings.git.defaultDiffDescription" => ( + "选择 Git 差异的默认布局", + "Choose the default layout for Git diffs", + ), + "settings.git.unified" => ("统一视图", "Unified"), + "settings.git.split" => ("拆分视图", "Split"), + "settings.git.editor" => ("编辑器", "Editor"), + "settings.git.inlineBlame" => ("启用行内 Blame", "Enable Inline Blame"), + "settings.git.inlineBlameDescription" => ( + "在编辑器中显示当前行的 Git Blame 元数据", + "Show inline Git blame metadata for the current line in the editor", + ), + "settings.git.executable" => ("Git 可执行文件", "Git executable"), + "settings.git.executableDescription" => ("留空则使用 PATH 中的 Git", "Use Git from PATH"), + "settings.git.execution" => ("Git 执行", "Git execution"), + "settings.git.detect" => ("探测", "Detect"), + "settings.git.useCredentialHelper" => ("使用凭据助手", "Use credential helper"), + // ---- 设置对话框内容:Git Fetch 默认行为(与 Tauri git-execution-settings 对齐) ---- + "git.fetch.defaults" => ("Fetch 默认行为", "Fetch defaults"), + "git.fetch.prune" => ( + "清理失效的远程跟踪引用", + "Prune stale remote-tracking references", + ), + "git.fetch.scope" => ( + "用于所有项目中的普通 Fetch。", + "Used for ordinary Fetch in every project.", + ), + "git.fetch.submodules" => ("获取子模块", "Fetch submodules"), + "git.fetch.submodules.inherit" => ("使用 Git 配置", "Use Git configuration"), + "git.fetch.submodules.no" => ("不获取子模块", "Do not fetch submodules"), + "git.fetch.submodules.onDemand" => ("按需获取", "Fetch on demand"), + "git.fetch.submodules.yes" => ("获取全部子模块", "Fetch all submodules"), + "git.fetch.tags" => ("获取标签", "Fetch tags"), + "git.fetch.tags.inherit" => ("使用 Git 配置", "Use Git configuration"), + "git.fetch.tags.all" => ("获取全部标签", "Fetch all tags"), + "git.fetch.tags.none" => ("不获取标签", "Do not fetch tags"), + "git.fetch.tags.prune" => ( + "同步标签并删除远程已不存在的本地标签", + "Synchronize tags and delete local tags absent on remote", + ), + "git.fetch.credentials" => ( + "凭据沿用现有 Git 凭据助手和 SSH 配置。", + "Credentials use your existing Git helper and SSH configuration.", + ), + // ---- 设置对话框内容:日志(与 Tauri log-settings-panel.tsx 对齐) ---- + "settings.logs.locations" => ("日志位置", "Log Locations"), + "settings.logs.effectivePath" => ("当前日志位置", "Current log location"), + "settings.logs.effectivePathDescription" => ( + "本次会话实际写入日志的目录。", + "The directory receiving logs in this session.", + ), + "settings.logs.defaultPath" => ("默认日志位置", "Default log location"), + "settings.logs.defaultPathDescription" => ( + "Lithe 始终维护并清理这个应用自有目录。", + "Lithe always maintains and cleans this application-owned directory.", + ), + "settings.logs.customPath" => ("自定义日志位置", "Custom log location"), + "settings.logs.customPathDescription" => ( + "选择父目录后,Lithe 会写入其中的 Lithe/logs 子目录。", + "Choose a parent directory; Lithe writes to its Lithe/logs subdirectory.", + ), + "settings.logs.choose" => ("选择…", "Choose…"), + "settings.logs.clear" => ("清除日志", "Clear Logs"), + "settings.logs.export" => ("导出", "Export"), + "settings.logs.diagnostics" => ("诊断", "Diagnostics"), + "settings.logs.diagnosticMode" => ( + "本次会话启用诊断日志", + "Diagnostic logging for this session", + ), + "settings.logs.diagnosticModeDescription" => ( + "记录 DEBUG 事件和低频 FPS 心跳;Lithe 重启后自动恢复为 INFO。", + "Record DEBUG events and a low-frequency FPS heartbeat until Lithe restarts.", + ), + "settings.logs.retention" => ("保留策略", "Retention"), + "settings.logs.retentionDescription" => ( + "单个日志达到 10 MB 后轮转,每天保留最新五个常规日志,并在 Lithe 启动时删除超过 30 天的日志。", + "Logs rotate at 10 MB, keep the newest five regular files per day, and are removed after 30 days when Lithe starts.", + ), + "settings.logs.clearCurrent" => ("清除当前目录日志", "Clear current directory logs"), + "settings.logs.clearCurrentDescription" => ( + "只删除当前目录中由 Lithe 管理的日志文件。", + "Delete only log files managed by Lithe in the active directory.", + ), + "settings.logs.clearLogs" => ("清除日志", "Clear Logs"), + "settings.logs.diagnosticBundle" => ("诊断包", "Diagnostic Bundle"), + "settings.logs.diagnosticBundleDescription" => ( + "导出包含脱敏应用日志和环境快照的压缩包,用于排查问题。", + "Export a zip with redacted application logs and an environment snapshot for troubleshooting.", + ), + "settings.logs.exportBundle" => ("导出诊断包…", "Export Diagnostic Bundle…"), + "settings.logs.exportBundleConfirm" => ("导出", "Export"), + // ---- 设置对话框内容:更新 ---- + "settings.mac.softwareUpdate" => ("软件更新", "Software Update"), + "settings.mac.currentVersion" => ("当前版本:{version}", "Current version: {version}"), + "settings.mac.checkForUpdates" => ("检查更新", "Check for Updates"), + "settings.mac.upToDate" => ("Lithe 当前已是最新版本。", "Lithe is up to date."), + "settings.mac.updateHint" => ( + "Lithe 可以检查新的预览版和稳定版。", + "Lithe can check for new preview and stable releases.", + ), + // ---- 设置对话框内容:项目工具链 / 运行生成 / AI 占位(Linux-frame) ---- + "settings.project.toolchain" => ("工具链", "Toolchain"), + "settings.run.generate" => ("生成运行配置", "Generate Run Configuration"), + "settings.run.empty" => ("暂无运行配置", "No run configurations"), + "settings.ai.noteTitle" => ("说明", "Note"), + "settings.ai.providerNote" => ( + "完整提供商与模型选择尚未接入,后续版本提供。", + "Full provider and model selection is not yet available.", + ), + "settings.ai.commitNote" => ( + "AI 提交配置尚未接入,后续版本提供。", + "AI commit configuration is not yet available.", + ), + "settings.ai.commitProfile" => ("配置文件", "Profile"), + "settings.ai.commitProfileDescription" => ( + "用于生成提交信息的模型配置", + "Model configuration for generating commit messages", + ), + "settings.ai.commitRules" => ("规则", "Rules"), + "settings.ai.commitRulesDescription" => ( + "生成提交信息时遵循的规则", + "Rules applied when generating commit messages", + ), + "settings.ai.defaultValue" => ("默认", "Default"), + "settings.aiCommit.enabled" => ("启用 AI 提交信息", "Enable AI commit messages"), + "settings.aiCommit.language" => ("输出语言", "Output language"), + "settings.aiCommit.format" => ("格式", "Format"), + "settings.aiCommit.includeBody" => ( + "有必要时包含简短正文", + "Include a short body when useful", + ), + "settings.aiCommit.subjectMax" => ("标题最大长度", "Maximum subject length"), + "settings.aiCommit.diffLimit" => ("Diff 字符限制", "Diff character budget"), + // ---- 新建项目对话框(与 Tauri locale.ts newProject.* 逐字对齐) ---- + "newProject.backToStarters" => ("返回项目模板", "Back to project starters"), + "newProject.repositoryUrl" => ("仓库 URL", "Repository URL"), + "newProject.repositoryUrlDescription" => ( + "支持 HTTPS 和 SSH 仓库 URL。", + "HTTPS and SSH repository URLs are supported.", + ), + "newProject.projectName" => ("项目名称", "Project Name"), + "newProject.location" => ("位置", "Location"), + "newProject.chooseParentFolder" => ("选择父文件夹", "Choose a parent folder"), + "newProject.chooseProjectLocation" => ("选择项目位置", "Choose project location"), + "newProject.packageManager" => ("包管理器", "Package Manager"), + "newProject.packageManagerDescription" => ( + "安装会在集成终端中运行,方便你查看输出。", + "Setup runs in the integrated terminal so you can follow its output.", + ), + "newProject.projectLocation" => ("项目位置", "Project location"), + "newProject.starters" => ("模板", "Starters"), + "newProject.errorEnterName" => ("请输入项目名称。", "Enter a project name."), + "newProject.errorPathTraversalName" => ( + "项目名称不能是路径穿越片段。", + "Project names cannot be path traversal segments.", + ), + "newProject.errorReservedCharacters" => ( + "项目名称不能包含路径分隔符或保留字符。", + "Project names cannot contain path separators or reserved characters.", + ), + "newProject.errorTrailingPeriodOrSpace" => ( + "项目名称不能以句点或空格结尾。", + "Project names cannot end with a period or space.", + ), + "newProject.errorCrossPlatformName" => ( + "请选择所有平台都支持的项目名称。", + "Choose a project name that is supported on every platform.", + ), + "newProject.errorDestinationExists" => ( + "{path} 已存在文件或文件夹。", + "A file or folder already exists at {path}.", + ), + "newProject.errorCreatedButCouldNotOpen" => ( + "项目已创建,但 Lithe 无法打开它。", + "The project was created but Lithe could not open it.", + ), + "newProject.cloningRepository" => ("正在克隆仓库", "Cloning Repository"), + "newProject.creatingProject" => ("正在创建项目", "Creating Project"), + "newProject.cloningRepositoryStatus" => ("正在克隆仓库", "Cloning repository"), + "newProject.preparingProject" => ("正在准备项目", "Preparing project"), + // ---- 新建项目来源(与 Tauri locale.ts welcome.* 逐字对齐) ---- + "welcome.newProject" => ("新建项目", "New Project"), + "welcome.chooseHowToStart" => ("选择如何开始...", "Choose how to start..."), + "welcome.emptyProject" => ("空项目", "Empty Project"), + "welcome.emptyProjectDescription" => ( + "创建一个空白文件夹,从零开始。", + "Create a clean folder and start from scratch.", + ), + "welcome.builtIn" => ("内置", "Built-in"), + "welcome.webApp" => ("Web 应用", "Web app"), + "welcome.nextjsDescription" => ( + "包含 App Router、TypeScript、Tailwind CSS、ESLint 和 src 目录。", + "App Router, TypeScript, Tailwind CSS, ESLint, and a src directory.", + ), + "welcome.viteReactDescription" => ( + "轻量的 React 和 TypeScript 起步模板。", + "A lightweight React and TypeScript starter.", + ), + "welcome.cloneRepository" => ("克隆仓库", "Clone Repository"), + "welcome.cloneRepositoryDescription" => ( + "将已有 Git 仓库克隆到本地新项目。", + "Clone an existing Git repository into a new local project.", + ), + "welcome.gitBadge" => ("Git", "Git"), + "welcome.noStarters" => ( + "没有匹配 “{query}” 的项目模板。", + "No project starters match \"{query}\".", + ), + "welcome.backToProjects" => ("返回项目列表", "Back to projects"), + _ => return "", + }; + if is_zh(cx) { + zh + } else { + en + } +} diff --git a/linux/src/keybindings.rs b/linux/src/keybindings.rs new file mode 100644 index 000000000..8fcd96193 --- /dev/null +++ b/linux/src/keybindings.rs @@ -0,0 +1,406 @@ +//! 快捷键真源:把「按键 → 动作 id」集中成一张表,供按键分发与菜单展示共用。 +//! +//! 背景(对齐 Tauri `features/keymaps`):Tauri 侧以 `defaultKeymaps` 为唯一真源, +//! 菜单项从注册表读取展示文本,按键事件也从同一张表解析动作,两者不会漂移。 +//! Linux 侧原先把快捷键硬编码在 `view.rs` 的 `on_key_down` 里,又另在 +//! `toolbar.rs` 的菜单表写一遍展示文本,存在两处真源。 +//! +//! 本模块把「菜单表已有的快捷键展示文本」作为唯一输入,编译期生成按键索引, +//! 因此新增菜单项时:只要菜单写了快捷键,按键就自动生效,不需要改两份代码。 +//! +//! 不变量: +//! - 同一个按键序列在本表中最多映射一个动作 id; +//! - 动作 id 必须能在 `WorkbenchView::handle_action` 找到分支,否则视为无效绑定; +//! - 双击 Shift 不是普通按键绑定(需要跨事件计时),单独由 +//! [`DoubleShiftRecognizer`] 处理,见 `view.rs` 的按键监听。 + +use std::collections::HashMap; + +use crate::workbench::toolbar::menu_shortcut_entries; + +/// 双击 Shift 触发全局搜索的时间窗(秒)。 +/// +/// 对齐 Tauri `DOUBLE_SHIFT_THRESHOLD_SECONDS`(IntelliJ 同款手势)。 +pub const DOUBLE_SHIFT_THRESHOLD_SECONDS: f64 = 0.35; + +/// 双击 Shift 对应的动作 id(Tauri `searchEverywhere`)。 +pub const DOUBLE_SHIFT_ACTION: &str = "view.global_search"; + +/// 一条按键绑定:展示用快捷键文本 + 目标动作 id。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct KeymapEntry { + /// 菜单里展示的写法(如 `Ctrl+Shift+P`),也是解析输入。 + pub shortcut: &'static str, + /// 稳定动作 id,与 `WorkbenchView::handle_action` 的分支一致。 + pub action: &'static str, +} + +/// 全局快捷键表,由菜单表派生,保证「菜单显示什么键,什么键就生效」。 +pub fn default_keymap() -> Vec { + menu_shortcut_entries() + .iter() + .map(|(shortcut, action)| KeymapEntry { shortcut, action }) + .collect() +} + +/// 解析后的按键索引:归一化按键序列 → 动作 id。 +/// +/// GPUI 侧查表用 [`KeyStrokeId`],避免每次按键都做字符串拼接。 +pub struct KeymapIndex { + by_id: HashMap, + /// 无法解析的展示文本(应当为空;保留用于诊断与测试断言)。 + invalid: Vec<&'static str>, +} + +impl KeymapIndex { + /// 从 [`default_keymap`] 构建索引。 + pub fn build() -> Self { + let mut by_id = HashMap::new(); + let mut invalid = Vec::new(); + for entry in default_keymap() { + match KeyStrokeId::parse(entry.shortcut) { + Some(id) => { + // 冲突时保留先出现的绑定,并按菜单顺序稳定覆盖: + // 菜单表本身若有重复键,后写会覆盖先写,这里显式记录。 + by_id.insert(id, entry.action); + } + None => invalid.push(entry.shortcut), + } + } + Self { by_id, invalid } + } + + /// 精确查表:修饰键与主键完全一致才算命中。 + pub fn lookup(&self, id: &KeyStrokeId) -> Option<&'static str> { + self.by_id.get(id).copied() + } + + /// 解析失败的展示文本(用于测试)。 + pub fn invalid_shortcuts(&self) -> &[&'static str] { + &self.invalid + } + + /// 已生效的绑定数量(用于测试)。 + pub fn len(&self) -> usize { + self.by_id.len() + } + + /// 是否为空表。 + pub fn is_empty(&self) -> bool { + self.by_id.is_empty() + } +} + +/// 归一化按键标识:修饰键集合 + 归一化主键名。 +/// +/// GPUI 的 `Keystroke.key` 对字母是小写、对符号是字面符号(如 `=`、`/`、`` ` ``), +/// 而菜单展示文本用 `Ctrl+=` 这类可读写法。这里把两侧都归一到同一形式。 +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct KeyStrokeId { + pub control: bool, + pub alt: bool, + pub shift: bool, + pub platform: bool, + /// 归一化主键:字母小写,`+`/`=`/`-` 等符号原样,功能键小写(`f11`)。 + pub key: String, +} + +impl KeyStrokeId { + /// 从菜单展示文本解析(如 `Ctrl+Shift+P`、`Alt+Up`、`F11`、`Ctrl+/`)。 + /// + /// 无法识别时返回 `None`,由调用方归入 `invalid` 报告。 + pub fn parse(shortcut: &str) -> Option { + let trimmed = shortcut.trim(); + if trimmed.is_empty() { + return None; + } + let mut control = false; + let mut alt = false; + let mut shift = false; + let mut platform = false; + let mut key: Option = None; + + for part in trimmed.split('+') { + if part.is_empty() { + // `Ctrl++`(加号本身)在菜单里写成 `Ctrl+=`,因此空段非法。 + return None; + } + match part { + "Ctrl" | "Control" | "CmdOrCtrl" => control = true, + "Alt" | "Option" => alt = true, + "Shift" => shift = true, + "Meta" | "Cmd" | "Command" | "Super" | "Win" => platform = true, + other => { + if key.is_some() { + // 多主键(含空格分段的序列)不由本表表达。 + return None; + } + key = Some(normalize_key(other)); + } + } + } + + let key = key?; + Some(Self { + control, + alt, + shift, + platform, + key, + }) + } + + /// 从 GPUI `KeyDownEvent` 的按键与修饰键状态构建。 + pub fn from_event(key: &str, modifiers: &gpui_kit::Modifiers) -> Self { + Self { + control: modifiers.control, + alt: modifiers.alt, + shift: modifiers.shift, + platform: modifiers.platform, + key: normalize_key(key), + } + } +} + +/// 主键名归一化:字母统一小写,其余原样;并把菜单里的可读名映射到 GPUI 名。 +fn normalize_key(key: &str) -> String { + match key { + "Up" => "up".to_string(), + "Down" => "down".to_string(), + "Left" => "left".to_string(), + "Right" => "right".to_string(), + "Enter" | "Return" => "enter".to_string(), + "Esc" | "Escape" => "escape".to_string(), + "Space" => "space".to_string(), + "Tab" => "tab".to_string(), + "Backspace" => "backspace".to_string(), + "Delete" => "delete".to_string(), + "Home" => "home".to_string(), + "End" => "end".to_string(), + "PageUp" => "pageup".to_string(), + "PageDown" => "pagedown".to_string(), + "`" | "~" => "`".to_string(), + "=" | "+" => "=".to_string(), + "-" | "_" => "-".to_string(), + other => other.to_ascii_lowercase(), + } +} + +/// 双击 Shift 手势识别器:移植 Tauri `DoubleShiftGestureRecognizer`。 +/// +/// 语义与 Tauri 一致: +/// - 只统计「独立按下 Shift」(按下时没有 Ctrl/Alt/Meta 同时按下); +/// - 两次独立 Shift 抬起的时间差落在 [`DOUBLE_SHIFT_THRESHOLD_SECONDS`] 内才算双击; +/// - 期间出现其它按键或非 Shift 修饰键会取消手势,避免误触。 +/// +/// 时间戳由调用方提供(GPUI 事件不带时间,`view.rs` 用 `Instant`)。 +#[derive(Debug, Default)] +pub struct DoubleShiftRecognizer { + shift_was_down: bool, + current_press_is_standalone: bool, + last_standalone_tap: Option, +} + +impl DoubleShiftRecognizer { + /// 新建识别器。 + pub fn new() -> Self { + Self::default() + } + + /// 普通按键按下:当前 Shift 按压不再算「独立」。 + pub fn handle_key_down(&mut self) { + self.current_press_is_standalone = false; + self.last_standalone_tap = None; + } + + /// 焦点切换/窗口失焦等场景重置,避免跨上下文的两次 Shift 被拼接。 + pub fn reset(&mut self) { + self.shift_was_down = false; + self.current_press_is_standalone = false; + self.last_standalone_tap = None; + } + + /// 修饰键状态变化。返回 `true` 表示识别到双击 Shift。 + /// + /// - `is_shift_down`:当前 Shift 是否按下; + /// - `has_other_modifiers`:是否有 Ctrl/Alt/Meta 同时按下; + /// - `timestamp`:秒级单调时间。 + pub fn handle_modifiers_changed( + &mut self, + is_shift_down: bool, + has_other_modifiers: bool, + timestamp: f64, + ) -> bool { + if is_shift_down && !self.shift_was_down { + self.current_press_is_standalone = !has_other_modifiers; + self.shift_was_down = true; + return false; + } + + if is_shift_down && self.shift_was_down { + if has_other_modifiers { + self.current_press_is_standalone = false; + self.last_standalone_tap = None; + } + return false; + } + + if !is_shift_down && self.shift_was_down { + self.shift_was_down = false; + let was_standalone = self.current_press_is_standalone; + self.current_press_is_standalone = false; + if !was_standalone || has_other_modifiers { + self.last_standalone_tap = None; + return false; + } + if let Some(last) = self.last_standalone_tap { + let delta = timestamp - last; + if delta >= 0.0 && delta < DOUBLE_SHIFT_THRESHOLD_SECONDS { + self.last_standalone_tap = None; + return true; + } + } + self.last_standalone_tap = Some(timestamp); + return false; + } + + if has_other_modifiers { + self.last_standalone_tap = None; + } + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_menu_shortcuts() { + let id = KeyStrokeId::parse("Ctrl+Shift+P").expect("parse"); + assert!(id.control && id.shift && !id.alt && !id.platform); + assert_eq!(id.key, "p"); + + let id = KeyStrokeId::parse("Alt+Up").expect("parse"); + assert!(id.alt); + assert_eq!(id.key, "up"); + + let id = KeyStrokeId::parse("F11").expect("parse"); + assert!(!id.control && id.key == "f11"); + + let id = KeyStrokeId::parse("Ctrl+/").expect("parse"); + assert_eq!(id.key, "/"); + + let id = KeyStrokeId::parse("Ctrl+=").expect("parse"); + assert_eq!(id.key, "="); + } + + #[test] + fn rejects_invalid_shortcuts() { + assert!(KeyStrokeId::parse("").is_none()); + assert!(KeyStrokeId::parse("Ctrl+A+B").is_none()); + assert!(KeyStrokeId::parse("Ctrl+").is_none()); + } + + #[test] + fn keymap_only_surfaces_menu_shortcuts() { + let index = KeymapIndex::build(); + assert!( + index.invalid_shortcuts().is_empty(), + "菜单快捷键存在无法解析的写法: {:?}", + index.invalid_shortcuts() + ); + assert!(!index.is_empty()); + } + + #[test] + fn keymap_lookup_matches_menu_labels() { + let index = KeymapIndex::build(); + let id = KeyStrokeId::parse("Ctrl+Shift+P").expect("parse"); + assert_eq!(index.lookup(&id), Some("view.command_palette")); + + let id = KeyStrokeId::parse("Ctrl+P").expect("parse"); + assert_eq!(index.lookup(&id), Some("view.quick_open")); + + let id = KeyStrokeId::parse("Ctrl+B").expect("parse"); + assert_eq!(index.lookup(&id), Some("view.toggle_activity_rail")); + + let id = KeyStrokeId::parse("Ctrl+E").expect("parse"); + assert_eq!(index.lookup(&id), Some("view.toggle_sidebar")); + + let id = KeyStrokeId::parse("Ctrl+Shift+F").expect("parse"); + assert_eq!(index.lookup(&id), Some("view.global_search")); + + let id = KeyStrokeId::parse("Alt+Z").expect("parse"); + assert_eq!(index.lookup(&id), Some("view.toggle_wrap")); + + let id = KeyStrokeId::parse("Ctrl+/ ").expect("parse"); + assert_eq!(index.lookup(&id), Some("edit.toggle_comment")); + + let id = KeyStrokeId::parse("Ctrl+T").expect("parse"); + assert_eq!(index.lookup(&id), Some("file.new_tab")); + + let id = KeyStrokeId::parse("Ctrl+W").expect("parse"); + assert_eq!(index.lookup(&id), Some("file.close_editor")); + + let id = KeyStrokeId::parse("F11").expect("parse"); + assert_eq!(index.lookup(&id), Some("window.fullscreen")); + } + + #[test] + fn disabled_menu_shortcuts_are_not_bound() { + let index = KeymapIndex::build(); + // `go.references`(禁用)与 `view.toggle_activity_rail` 共用 Ctrl+B: + // 禁用项不得抢走可用项的按键。 + let id = KeyStrokeId::parse("Ctrl+B").expect("parse"); + assert_ne!(index.lookup(&id), Some("go.references")); + // F5/F9 等调试快捷键后端未接入,不应绑定。 + assert_eq!(index.lookup(&KeyStrokeId::parse("F5").expect("parse")), None); + assert_eq!(index.lookup(&KeyStrokeId::parse("F9").expect("parse")), None); + } + + #[test] + fn double_shift_action_targets_global_search() { + assert_eq!(DOUBLE_SHIFT_ACTION, "view.global_search"); + } + + #[test] + fn double_shift_triggers_within_threshold() { + let mut r = DoubleShiftRecognizer::new(); + // 按下不触发,只有第二次独立 Shift 抬起且间隔在窗口内才触发。 + assert!(!r.handle_modifiers_changed(true, false, 1.0)); + assert!(!r.handle_modifiers_changed(false, false, 1.05)); + assert!(!r.handle_modifiers_changed(true, false, 1.1)); + assert!(r.handle_modifiers_changed(false, false, 1.2)); + } + + #[test] + fn double_shift_ignores_slow_taps() { + let mut r = DoubleShiftRecognizer::new(); + assert!(!r.handle_modifiers_changed(true, false, 1.0)); + assert!(!r.handle_modifiers_changed(false, false, 1.05)); + assert!(!r.handle_modifiers_changed(true, false, 2.0)); + assert!(!r.handle_modifiers_changed(false, false, 2.1)); + } + + #[test] + fn double_shift_cancelled_by_other_keys() { + let mut r = DoubleShiftRecognizer::new(); + assert!(!r.handle_modifiers_changed(true, false, 1.0)); + assert!(!r.handle_modifiers_changed(false, false, 1.05)); + // 中间敲了别的键,第二次 Shift 不应触发。 + r.handle_key_down(); + assert!(!r.handle_modifiers_changed(true, false, 1.1)); + assert!(!r.handle_modifiers_changed(false, false, 1.15)); + } + + #[test] + fn double_shift_cancelled_by_other_modifiers() { + let mut r = DoubleShiftRecognizer::new(); + assert!(!r.handle_modifiers_changed(true, false, 1.0)); + assert!(!r.handle_modifiers_changed(false, false, 1.05)); + // Shift 与 Ctrl 同按不算独立 Shift。 + assert!(!r.handle_modifiers_changed(true, true, 1.1)); + assert!(!r.handle_modifiers_changed(false, true, 1.15)); + } +} diff --git a/linux/src/lib.rs b/linux/src/lib.rs new file mode 100644 index 000000000..04970fa31 --- /dev/null +++ b/linux/src/lib.rs @@ -0,0 +1,9 @@ +pub mod core; +pub mod i18n; +pub mod keybindings; +pub mod lsp; +pub mod settings; +pub mod theme; +pub mod workbench; + +pub use workbench::WorkbenchView; diff --git a/linux/src/lsp.rs b/linux/src/lsp.rs new file mode 100644 index 000000000..14f6b5cdb --- /dev/null +++ b/linux/src/lsp.rs @@ -0,0 +1,1452 @@ +//! Linux 端 LSP 接线:把编辑器文档同步给 Core 管理的语言服务器,并把 +//! `lsp.pollEvents` 返回的诊断映射到诊断面板。 +//! +//! Core 拥有进程、JSON-RPC framing、生命周期和诊断状态;本模块只做 +//! 平台侧的语言/可执行文件探测、路径与 URI 规范化,以及事件投影。 + +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; + +use serde::Serialize; +use serde_json::{json, Value}; + +use crate::workbench::bottom_panel::DiagnosticEntry; + +/// 一个可在本机通过 PATH 启动的语言服务器定义。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LanguageProvider { + /// 与 Core `providerId` 一致的稳定标识。 + pub id: &'static str, + /// PATH 中查找的可执行文件名。 + pub executable: &'static str, + /// 以 stdio 启动服务器所需的固定参数。 + pub arguments: &'static [&'static str], + /// 对应文件扩展名的 LSP `languageId` 前缀。 + pub language_id: &'static str, + /// 扩展名匹配集合,全部小写且不含点。 + pub extensions: &'static [&'static str], +} + +/// Core 对 JDT LS 使用的 provider 标识;不要使用发行包 wrapper 的 `jdtls` 名称。 +pub const JAVA_PROVIDER_ID: &str = "java"; + +const JDTLS_EQUINOX_PREFIX: &str = "org.eclipse.equinox.launcher_"; +const JDTLS_DEBUG_PREFIX: &str = "com.microsoft.java.debug.plugin-"; +const JDTLS_TEST_LIST: &str = "extensions.txt"; +const MAX_BUILD_FILE_DEPTH: usize = 32; + +fn linux_configuration_name() -> &'static str { + match std::env::consts::ARCH { + "aarch64" | "arm" => "config_linux_arm", + _ => "config_linux", + } +} + +/// Linux 解析出的 JDT LS 直启资源。 +/// +/// 这些路径只属于 Linux 适配器;Core 负责把它们转换为 JVM 参数和 JDT 初始化选项。 +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct JdtlsLaunchResources { + /// Equinox launcher JAR。 + pub launcher_jar_path: String, + /// Linux Eclipse configuration 目录。 + pub configuration_directory: String, + /// Lombok agent JAR。 + pub lombok_agent_path: String, + /// Java Debug Server bundle JAR。 + pub java_debug_bundle_path: String, + /// Java Test extension bundles,按稳定路径顺序提交。 + pub java_extension_bundle_paths: Vec, +} + +/// Linux 主机解析出的 Java 语言服务器启动计划。 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct JavaLspLaunch { + /// Core provider 必须是 `java`。 + pub provider_id: String, + /// LSP language id。 + pub language_id: String, + /// 直接启动时为 bundled Java,兼容 wrapper 时为 wrapper 路径。 + pub executable_path: String, + /// 外部 wrapper 的兼容参数;直启资源由 Core 适配。 + pub arguments: Vec, + /// JDT LS 使用的 Java 可执行文件。 + pub runtime_executable_path: Option, + /// JDT LS 直启资源;外部 wrapper 回退时不提交该字段。 + pub jdtls_launch_resources: Option, + /// JDT LS 的 JAVA_HOME。 + pub java_home_path: Option, + /// 用于 Core workspace fingerprint 的发行包版本。 + pub jdtls_version: String, +} + +/// 一条已由 Workbench 轮询收到的 Core LSP 请求结果。 +#[derive(Debug, Clone)] +pub struct LspOperationResult { + /// Core 事件所属的会话。 + pub session_id: String, + /// 成功结果;失败时为空。 + pub result: Option, + /// Core 返回的稳定错误码、消息和诊断详情。 + pub error: Option, +} + +/// Run 读取的 Java session 快照。 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct JavaLspStatus { + /// 当前 Core session;没有会话时为空。 + pub session_id: Option, + /// `idle`、`starting`、`initializing`、`ready`、`failed` 或 `stopped`。 + pub state: String, + /// 当前工作区最近一次 Java session 错误。 + pub error: Option, + /// Core 轮询返回的项目准备快照。 + pub project_preparation: Option, + /// 拒绝旧工作区回调的代数。 + pub generation: u64, +} + +const RUST_ANALYZER: LanguageProvider = LanguageProvider { + id: "rust-analyzer", + executable: "rust-analyzer", + arguments: &[], + language_id: "rust", + extensions: &["rs"], +}; + +const CLANGD: LanguageProvider = LanguageProvider { + id: "clangd", + executable: "clangd", + arguments: &[], + language_id: "c", + extensions: &["c", "h", "cc", "cpp", "cxx", "hpp", "hh", "hxx"], +}; + +const PYRIGHT: LanguageProvider = LanguageProvider { + id: "pyright", + executable: "pyright-langserver", + arguments: &["--stdio"], + language_id: "python", + extensions: &["py", "pyi"], +}; + +const TYPESCRIPT: LanguageProvider = LanguageProvider { + id: "typescript-language-server", + executable: "typescript-language-server", + arguments: &["--stdio"], + language_id: "typescript", + extensions: &["ts", "tsx", "js", "jsx", "mjs", "cjs"], +}; + +const GOPLS: LanguageProvider = LanguageProvider { + id: "gopls", + executable: "gopls", + arguments: &[], + language_id: "go", + extensions: &["go"], +}; + +const JDTLS: LanguageProvider = LanguageProvider { + id: "java", + executable: "jdtls", + arguments: &[], + language_id: "java", + extensions: &["java"], +}; + +const PROVIDERS: &[LanguageProvider] = &[RUST_ANALYZER, CLANGD, PYRIGHT, TYPESCRIPT, GOPLS, JDTLS]; + +/// 按文件扩展名选择语言服务器;未知扩展名返回 `None`。 +pub fn provider_for_path(path: &str) -> Option { + let extension = file_extension(path)?; + PROVIDERS + .iter() + .copied() + .find(|provider| provider.extensions.contains(&extension.as_str())) +} + +fn file_extension(path: &str) -> Option { + std::path::Path::new(path) + .extension() + .and_then(|value| value.to_str()) + .map(|value| value.to_ascii_lowercase()) +} + +/// provider 的默认 `languageId` 对同族扩展名需要细分,例如 clangd 的 C/C++ +/// 与 typescript-language-server 的 TS/JS 使用不同的语言标识。 +pub fn language_id_for_path(path: &str, provider: LanguageProvider) -> &'static str { + let extension = file_extension(path).unwrap_or_default(); + match provider.id { + "clangd" => match extension.as_str() { + "cc" | "cpp" | "cxx" | "hpp" | "hh" | "hxx" => "cpp", + _ => "c", + }, + "typescript-language-server" => match extension.as_str() { + "js" | "jsx" | "mjs" | "cjs" => "javascript", + _ => "typescript", + }, + _ => provider.language_id, + } +} + +/// 在 `PATH` 中查找可执行文件;只返回可执行且存在的绝对路径。 +pub fn find_in_path(name: &str) -> Option { + let path = std::env::var_os("PATH")?; + for dir in std::env::split_paths(&path) { + let candidate = dir.join(name); + if !candidate.is_file() { + continue; + } + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt as _; + let executable = std::fs::metadata(&candidate) + .map(|metadata| metadata.permissions().mode() & 0o111 != 0) + .unwrap_or(false); + if !executable { + continue; + } + } + return Some(candidate.to_string_lossy().into_owned()); + } + None +} + +/// 解析 Linux Java 语言服务器资源。 +/// +/// 正式运行路径优先使用工作区 `.artifacts` 或环境覆盖;只有正式目录不存在时, +/// 才允许回退到外部 `jdtls` wrapper。正式目录存在但资源不完整时直接返回错误, +/// 避免悄悄退回另一套 JDT 安装。 +pub fn resolve_java_lsp_launch(root: &str) -> Result { + let workspace = Path::new(root); + let explicit_jdtls_root = std::env::var_os("LITHE_JDTLS_ROOT") + .map(PathBuf::from) + .filter(|path| !path.as_os_str().is_empty()); + let jdtls_root = explicit_jdtls_root + .clone() + .unwrap_or_else(|| workspace.join(".artifacts").join("jdtls-linux")); + let explicit_jdk_root = std::env::var_os("LITHE_JDK_ROOT") + .map(PathBuf::from) + .filter(|path| !path.as_os_str().is_empty()); + let jdk_root = explicit_jdk_root + .clone() + .unwrap_or_else(|| workspace.join(".artifacts").join("jdk-linux")); + + if jdtls_root.exists() { + let resources = resolve_direct_jdtls_resources(&jdtls_root)?; + let java_executable = resolve_java_executable(&jdk_root).ok_or_else(|| { + format!( + "Java language-server runtime is missing. Set LITHE_JDK_ROOT or provide {}/bin/java.", + jdk_root.display() + ) + })?; + let java_home = Path::new(&java_executable) + .parent() + .and_then(Path::parent) + .map(|path| path.to_string_lossy().into_owned()); + return Ok(JavaLspLaunch { + provider_id: JAVA_PROVIDER_ID.to_string(), + language_id: JAVA_PROVIDER_ID.to_string(), + executable_path: java_executable.clone(), + arguments: Vec::new(), + runtime_executable_path: Some(java_executable), + jdtls_launch_resources: Some(resources), + java_home_path: java_home, + jdtls_version: read_jdtls_version(&jdtls_root), + }); + } + + if explicit_jdtls_root.is_some() { + return Err(format!( + "JDT LS resource root does not exist: {}. Set LITHE_JDTLS_ROOT to a complete JDT LS installation.", + jdtls_root.display() + )); + } + + resolve_wrapper_java_lsp_launch(&jdk_root) +} + +fn resolve_direct_jdtls_resources(root: &Path) -> Result { + let launcher_jar_path = first_sorted_file(root.join("plugins"), JDTLS_EQUINOX_PREFIX, ".jar")? + .ok_or_else(|| { + format!( + "JDT LS launcher is missing under {}. Expected {}*.jar.", + root.join("plugins").display(), + JDTLS_EQUINOX_PREFIX + ) + })?; + let configuration_directory = root.join(linux_configuration_name()); + if !configuration_directory.is_dir() { + return Err(format!( + "JDT LS Linux configuration is missing: {}", + configuration_directory.display() + )); + } + let lombok_agent_path = root.join("lombok").join("lombok.jar"); + if !lombok_agent_path.is_file() { + return Err(format!( + "JDT LS Lombok agent is missing: {}", + lombok_agent_path.display() + )); + } + let java_debug_bundle_path = + first_sorted_file(root.join("java-debug"), JDTLS_DEBUG_PREFIX, ".jar")?.ok_or_else( + || { + format!( + "JDT LS Java Debug bundle is missing under {}.", + root.join("java-debug").display() + ) + }, + )?; + let java_extension_bundle_paths = resolve_java_test_bundles(root)?; + Ok(JdtlsLaunchResources { + launcher_jar_path: path_string(&launcher_jar_path), + configuration_directory: path_string(&configuration_directory), + lombok_agent_path: path_string(&lombok_agent_path), + java_debug_bundle_path: path_string(&java_debug_bundle_path), + java_extension_bundle_paths: java_extension_bundle_paths + .iter() + .map(|path| path_string(path)) + .collect(), + }) +} + +fn resolve_java_test_bundles(root: &Path) -> Result, String> { + let directory = root.join("java-test").join("extensions"); + if !directory.is_dir() { + return Err(format!( + "JDT LS Java Test extensions are missing: {}", + directory.display() + )); + } + let list_path = root.join("java-test").join(JDTLS_TEST_LIST); + let mut bundles = match fs::read_to_string(&list_path) { + Ok(text) => { + let mut values = Vec::new(); + for name in text.lines().map(str::trim).filter(|name| !name.is_empty()) { + if name.contains(['/', '\\']) || name == ".." { + return Err(format!( + "JDT LS Java Test bundle list contains a path instead of a file name: {name}" + )); + } + let path = directory.join(name); + if !path.is_file() { + return Err(format!( + "JDT LS Java Test bundle is missing: {}", + path.display() + )); + } + values.push(path); + } + values + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + let entries = fs::read_dir(&directory).map_err(|error| { + format!( + "Failed to inspect JDT LS Java Test bundles at {}: {error}", + directory.display() + ) + })?; + entries + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| { + path.is_file() && path.extension().and_then(|ext| ext.to_str()) == Some("jar") + }) + .collect() + } + Err(error) => { + return Err(format!( + "Failed to read JDT LS Java Test bundle list at {}: {error}", + list_path.display() + )) + } + }; + bundles.sort(); + bundles.dedup(); + if bundles.is_empty() { + return Err(format!( + "JDT LS Java Test extension directory contains no bundles: {}", + directory.display() + )); + } + Ok(bundles) +} + +fn first_sorted_file( + directory: PathBuf, + prefix: &str, + suffix: &str, +) -> Result, String> { + let entries = match fs::read_dir(&directory) { + Ok(entries) => entries, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(error) => { + return Err(format!( + "Failed to inspect JDT LS resources at {}: {error}", + directory.display() + )) + } + }; + let mut files = entries + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| { + path.is_file() + && path + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.starts_with(prefix) && name.ends_with(suffix)) + }) + .collect::>(); + files.sort(); + Ok(files.into_iter().next()) +} + +fn resolve_java_executable(jdk_root: &Path) -> Option { + let candidate = jdk_root.join("bin").join("java"); + candidate.is_file().then(|| path_string(&candidate)) +} + +fn resolve_wrapper_java_lsp_launch(jdk_root: &Path) -> Result { + let mut candidates = Vec::new(); + if let Some(executable) = std::env::var_os("LITHE_JDTLS_EXECUTABLE") { + candidates.push(PathBuf::from(executable)); + } + if let Some(home) = std::env::var_os("JDTLS_HOME") { + let home = PathBuf::from(home); + candidates.push(home.join("bin").join("jdtls")); + candidates.push(home.join("jdtls")); + } + for name in ["jdtls", "jdtls.py"] { + if let Some(executable) = find_in_path(name) { + candidates.push(PathBuf::from(executable)); + } + } + let executable = candidates + .into_iter() + .find(|candidate| candidate.is_file()) + .ok_or_else(|| { + "JDT LS is not installed. Set LITHE_JDTLS_ROOT to the bundled resources or LITHE_JDTLS_EXECUTABLE to a compatible wrapper." + .to_string() + })?; + let runtime = resolve_java_executable(jdk_root) + .or_else(|| { + std::env::var_os("JAVA_HOME").and_then(|home| { + let path = PathBuf::from(home).join("bin").join("java"); + path.is_file().then(|| path_string(&path)) + }) + }) + .or_else(|| find_in_path("java")); + let java_home = runtime + .as_deref() + .and_then(|path| Path::new(path).parent()) + .and_then(Path::parent) + .map(|path| path.to_string_lossy().into_owned()); + Ok(JavaLspLaunch { + provider_id: JAVA_PROVIDER_ID.to_string(), + language_id: JAVA_PROVIDER_ID.to_string(), + executable_path: path_string(&executable), + arguments: Vec::new(), + runtime_executable_path: runtime, + jdtls_launch_resources: None, + java_home_path: java_home, + jdtls_version: "external-wrapper".to_string(), + }) +} + +fn read_jdtls_version(root: &Path) -> String { + fs::read_to_string(root.join("manifest.json")) + .ok() + .and_then(|text| serde_json::from_str::(&text).ok()) + .and_then(|value| { + value + .get("version") + .and_then(Value::as_str) + .map(str::to_string) + }) + .filter(|version| !version.trim().is_empty()) + .unwrap_or_else(|| "direct-resources".to_string()) +} + +fn path_string(path: &Path) -> String { + path.to_string_lossy().replace('\\', "/") +} + +/// 生成带 JDT LS 资源和 Core 所需上下文的 `lsp.startServer` 载荷。 +pub fn java_start_payload( + launch: &JavaLspLaunch, + root: &str, + cache_directory: &str, + workspace_fingerprint: Option<&str>, + maven_context: Option<&Value>, +) -> Value { + let mut payload = json!({ + "providerId": launch.provider_id, + "executablePath": launch.executable_path, + "arguments": launch.arguments, + "rootUri": file_uri(root, root), + "workingDirectory": absolute_path(root, root), + "runtimeExecutablePath": launch.runtime_executable_path, + "initializeTimeoutMilliseconds": 30_000, + "requestTimeoutMilliseconds": 30_000, + "shutdownTimeoutMilliseconds": 2_000, + "cacheDirectory": cache_directory, + }); + if let Some(resources) = &launch.jdtls_launch_resources { + payload["jdtlsLaunchResources"] = + serde_json::to_value(resources).expect("resources encode"); + } + if let Some(java_home) = &launch.java_home_path { + payload["environment"] = json!({ "JAVA_HOME": java_home }); + } + if let Some(fingerprint) = workspace_fingerprint.filter(|value| !value.is_empty()) { + payload["workspaceFingerprint"] = json!(fingerprint); + } + if let Some(context) = maven_context { + payload["mavenContext"] = context.clone(); + } + payload +} + +/// 为 JDT LS 提供最小且可序列化的 Maven 上下文。 +/// +/// Linux 不从 Maven 命令行或源码推断项目模型;Core 会用这个上下文配置 JDT 的 +/// settings、Profile 和递归模块导入。缺少机器本地 Maven 选择时保留空值。 +pub fn maven_context_for_workspace(root: &str) -> Option { + let workspace = Path::new(root); + let reactor = workspace.ancestors().find_map(|directory| { + directory + .strip_prefix(workspace) + .ok() + .filter(|_| directory.join("pom.xml").is_file()) + .map(|_| directory) + })?; + let reactor_path = reactor + .strip_prefix(workspace) + .ok() + .map(|path| path.to_string_lossy().replace('\\', "/")) + .filter(|path| !path.is_empty()) + .unwrap_or_else(|| ".".to_string()); + let local_repository = std::env::var_os("LITHE_MAVEN_LOCAL_REPOSITORY") + .map(PathBuf::from) + .filter(|path| path.is_absolute()); + let settings = std::env::var_os("LITHE_MAVEN_SETTINGS") + .map(PathBuf::from) + .filter(|path| path.is_absolute()); + let java_home = std::env::var_os("LITHE_JDK_ROOT") + .map(PathBuf::from) + .filter(|path| path.is_absolute()) + .or_else(|| { + let bundled = workspace.join(".artifacts").join("jdk-linux"); + bundled + .join("bin") + .join("java") + .is_file() + .then_some(bundled) + }); + let maven_executable = std::env::var_os("LITHE_MAVEN_EXECUTABLE") + .map(PathBuf::from) + .filter(|path| path.is_absolute()); + Some(json!({ + "version": 1, + "reactorPath": reactor_path, + "profiles": Vec::::new(), + "settingsPath": settings.map(|path| path_string(&path)), + "localRepositoryPath": local_repository.map(|path| path_string(&path)), + "skipTests": false, + "mavenExecutablePath": maven_executable.map(|path| path_string(&path)), + "javaHomePath": java_home.map(|path| path_string(&path)), + })) +} + +/// 构造 Core `java.jdtWorkspaceFingerprint` 的平台观察载荷。 +pub fn workspace_fingerprint_request(root: &str, jdtls_version: &str) -> Value { + let workspace = Path::new(root); + let mut build_files = Vec::new(); + let mut direct_maven_modules = Vec::new(); + collect_build_observations( + workspace, + workspace, + 0, + &mut build_files, + &mut direct_maven_modules, + ); + build_files.sort_by(|left, right| left["path"].as_str().cmp(&right["path"].as_str())); + direct_maven_modules.sort(); + direct_maven_modules.dedup(); + json!({ + "buildFiles": build_files, + "directMavenModules": direct_maven_modules, + "jdtlsVersion": jdtls_version, + }) +} + +fn collect_build_observations( + root: &Path, + directory: &Path, + depth: usize, + build_files: &mut Vec, + modules: &mut Vec, +) { + if depth > MAX_BUILD_FILE_DEPTH { + return; + } + let Ok(entries) = fs::read_dir(directory) else { + return; + }; + for entry in entries.filter_map(Result::ok) { + let path = entry.path(); + let name = entry.file_name().to_string_lossy().into_owned(); + let Ok(file_type) = entry.file_type() else { + continue; + }; + if file_type.is_dir() { + if name.starts_with('.') || matches!(name.as_str(), "target" | "build" | "node_modules") + { + continue; + } + collect_build_observations(root, &path, depth + 1, build_files, modules); + continue; + } + if !file_type.is_file() || !is_build_file(&name) { + continue; + } + let Ok(relative) = path.strip_prefix(root) else { + continue; + }; + let Ok(metadata) = entry.metadata() else { + continue; + }; + let modified = metadata + .modified() + .ok() + .and_then(|value| value.duration_since(std::time::UNIX_EPOCH).ok()) + .map(|value| value.as_millis() as u64) + .unwrap_or(0); + build_files.push(json!({ + "path": relative.to_string_lossy().replace('\\', "/"), + "modifiedUnixMilliseconds": modified, + "sizeBytes": metadata.len(), + })); + if name == "pom.xml" { + collect_maven_modules(&path, root, modules); + } + } +} + +fn is_build_file(name: &str) -> bool { + matches!( + name, + "pom.xml" + | "build.gradle" + | "build.gradle.kts" + | "settings.gradle" + | "settings.gradle.kts" + | "gradle.properties" + ) +} + +fn collect_maven_modules(path: &Path, root: &Path, output: &mut Vec) { + let Ok(text) = fs::read_to_string(path) else { + return; + }; + let mut remaining = text.as_str(); + while let Some(start) = remaining.find("') else { + break; + }; + let value = &after[open_end + 1..]; + let Some(close) = value.find("") else { + break; + }; + let module = value[..close].trim(); + if !module.is_empty() && module != "." && !module.contains("..") { + let module_path = path.parent().unwrap_or(root).join(module); + if let Ok(relative) = module_path.strip_prefix(root) { + let relative = relative.to_string_lossy().replace('\\', "/"); + if !relative.contains('/') && relative != "." { + output.push(relative); + } + } + } + remaining = &value[close + "".len()..]; + } +} + +/// 判断 Run 是否可以消费 Java 准备结果。 +/// +/// Core 的 `ServiceReady` 只表示协议和项目导入完成;Maven Profile 等准备任务 +/// 仍由 `projectPreparation.blocksRun` 门禁。Core 允许非阻塞的部分 Profile +/// 失败继续进入目标项目构建,Linux 不在 UI 侧另造一套状态机。 +pub fn java_lsp_allows_run(status: &JavaLspStatus) -> bool { + if status.state != "ready" { + return false; + } + status + .project_preparation + .as_ref() + .and_then(|value| value.get("blocksRun")) + .and_then(Value::as_bool) + .is_some_and(|value| !value) +} + +/// Core Java 入口发现请求。 +pub fn java_entrypoints_request(session_id: &str) -> Value { + json!({ + "sessionId": session_id, + "operation": "javaEntrypoints", + }) +} + +/// Core `vscode.java.buildWorkspace` 请求。 +pub fn java_build_request( + session_id: &str, + source_path: &str, + main_class: &str, + project_name: Option<&str>, +) -> Value { + let arguments = serde_json::to_string(&json!({ + "mainClass": main_class, + "projectName": project_name, + "filePath": source_path, + "isFullBuild": false, + })) + .expect("Java build command encodes"); + json!({ + "sessionId": session_id, + "operation": "executeCommand", + "command": { + "title": "Build Java Workspace", + "command": "vscode.java.buildWorkspace", + "arguments": [arguments], + }, + }) +} + +/// Core `vscode.java.resolveClasspath` 请求。 +pub fn java_classpath_request( + session_id: &str, + main_class: &str, + project_name: Option<&str>, +) -> Value { + json!({ + "sessionId": session_id, + "operation": "executeCommand", + "command": { + "title": "Resolve Java Runtime Classpath", + "command": "vscode.java.resolveClasspath", + "arguments": [main_class, project_name.unwrap_or(""), "runtime"], + }, + }) +} + +/// 校验 Core 的 Java 入口结果并保留其稳定 workspace-relative 结构。 +pub fn parse_java_entrypoints(value: &Value) -> Result { + let value = unwrap_command_value(value); + let entries = value + .get("entries") + .and_then(Value::as_array) + .ok_or_else(|| "Java language service returned no entry-point list.".to_string())?; + let schema_version = value + .get("schemaVersion") + .and_then(Value::as_u64) + .ok_or_else(|| "Java entry-point result has no schema version.".to_string())?; + if schema_version != 1 { + return Err("Unsupported Java entry-point schema version.".to_string()); + } + let mut entries = entries.clone(); + entries.sort_by(|left, right| { + left.get("sourcePath") + .and_then(Value::as_str) + .cmp(&right.get("sourcePath").and_then(Value::as_str)) + .then_with(|| { + left.get("mainClass") + .and_then(Value::as_str) + .cmp(&right.get("mainClass").and_then(Value::as_str)) + }) + }); + entries.dedup(); + let normalized = json!({ + "schemaVersion": schema_version, + "entries": entries, + "diagnostics": value.get("diagnostics").cloned().unwrap_or_else(|| json!([])), + }); + Ok(normalized) +} + +/// 从 Core 的入口列表选择配置对应的唯一目标,不读取 Java 源码。 +pub fn select_java_entrypoint( + value: &Value, + source_path: &str, + configured_main_class: Option<&str>, +) -> Result { + let entries = value + .get("entries") + .and_then(Value::as_array) + .ok_or_else(|| "Java entry-point result has no entries.".to_string())?; + let source_matches = entries + .iter() + .filter(|entry| entry.get("sourcePath").and_then(Value::as_str) == Some(source_path)) + .collect::>(); + let selected = if source_matches.len() == 1 { + source_matches[0].clone() + } else { + source_matches + .into_iter() + .find(|entry| { + configured_main_class.is_some_and(|main| { + entry.get("mainClass").and_then(Value::as_str) == Some(main) + }) + }) + .cloned() + .ok_or_else(|| { + format!( + "Java language service could not identify one launch target for {source_path}." + ) + })? + }; + if selected.get("mainClass").and_then(Value::as_str).is_none() { + return Err("Java entry-point result is missing mainClass.".to_string()); + } + Ok(selected) +} + +/// 校验 Core `vscode.java.buildWorkspace` 的成功结果。 +pub fn parse_java_build_result(value: &Value) -> Result<(), String> { + let value = unwrap_command_value(value); + let status = value + .as_i64() + .or_else(|| value.get("value").and_then(Value::as_i64)); + if status == Some(1) { + Ok(()) + } else { + Err("Java language service returned an unexpected project build status.".to_string()) + } +} + +/// 解析 Core `vscode.java.resolveClasspath` 的 `[modulePaths, classPaths]`。 +pub fn parse_java_classpath(value: &Value) -> Result<(Vec, Vec), String> { + let value = unwrap_command_value(value); + let paths = value.as_array().ok_or_else(|| { + "Java language service returned an invalid runtime classpath.".to_string() + })?; + if paths.len() != 2 { + return Err( + "Java language service returned an invalid runtime classpath shape.".to_string(), + ); + } + let module_paths = string_array(paths[0].clone()) + .ok_or_else(|| "Java module paths must be an array of strings.".to_string())?; + let class_paths = string_array(paths[1].clone()) + .ok_or_else(|| "Java class paths must be an array of strings.".to_string())?; + if module_paths.is_empty() && class_paths.is_empty() { + return Err("Java language service returned no runtime paths.".to_string()); + } + Ok((module_paths, class_paths)) +} + +/// 组合 JDT 返回的目标和路径;Linux 不在这里拼接 classpath 字符串。 +pub fn java_launch_payload(entrypoint: &Value, classpath: &Value) -> Result { + let main_class = entrypoint + .get("mainClass") + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + .ok_or_else(|| "Java entry-point result is missing mainClass.".to_string())?; + let (module_paths, class_paths) = parse_java_classpath(classpath)?; + Ok(json!({ + "mainClass": main_class, + "projectName": entrypoint.get("projectName").cloned().unwrap_or(Value::Null), + "classPaths": class_paths, + "modulePaths": module_paths, + })) +} + +fn unwrap_command_value(value: &Value) -> &Value { + value.get("value").unwrap_or(value) +} + +fn string_array(value: Value) -> Option> { + value + .as_array()? + .iter() + .map(|item| item.as_str().map(str::to_string)) + .collect() +} + +/// 从一次 `lsp.pollEvents` 响应中提取属于指定会话的请求结果。 +pub fn operation_results_from_poll( + value: &Value, + session_id: &str, +) -> Vec<(String, LspOperationResult)> { + value + .get("events") + .and_then(Value::as_array) + .into_iter() + .flatten() + .filter(|event| { + event.get("type").and_then(Value::as_str) == Some("requestCompleted") + && event.get("sessionId").and_then(Value::as_str) == Some(session_id) + }) + .filter_map(|event| { + let operation_id = event + .get("operationId") + .and_then(Value::as_str)? + .to_string(); + let error = event.get("error").and_then(format_lsp_error); + Some(( + operation_id.clone(), + LspOperationResult { + session_id: session_id.to_string(), + result: event.get("result").cloned(), + error, + }, + )) + }) + .collect() +} + +fn format_lsp_error(error: &Value) -> Option { + let code = error + .get("code") + .and_then(Value::as_str) + .unwrap_or("lspError"); + let message = error + .get("message") + .and_then(Value::as_str) + .unwrap_or("Language-server request failed."); + let mut text = format!("{code}: {message}"); + if let Some(details) = error + .get("details") + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + { + text.push_str(&format!(" ({details})")); + } + if let Some(report) = error.get("javaBuildReport") { + text.push_str(&format!( + " [javaBuildReport={}]", + serde_json::to_string(report).unwrap_or_else(|_| "{}".to_string()) + )); + } + Some(text) +} + +/// 读取当前 Java session 的可观察生命周期状态。 +#[allow(dead_code)] +pub fn java_lifecycle_state(value: &Value) -> Option { + java_lifecycle_state_for(value, None) +} + +/// 读取指定 Java session 的最新生命周期状态。 +pub fn java_lifecycle_state_for(value: &Value, session_id: Option<&str>) -> Option { + value + .get("events") + .and_then(Value::as_array)? + .iter() + .rev() + .find_map(|event| { + let session_matches = session_id.is_none_or(|session_id| { + event.get("sessionId").and_then(Value::as_str) == Some(session_id) + }); + (event.get("type").and_then(Value::as_str) == Some("stateChanged") + && event.get("providerId").and_then(Value::as_str) == Some(JAVA_PROVIDER_ID) + && session_matches) + .then(|| { + event + .get("state") + .and_then(Value::as_str) + .map(str::to_string) + }) + .flatten() + }) +} + +pub fn absolute_path(root: &str, path: &str) -> String { + let candidate = std::path::Path::new(path); + if candidate.is_absolute() { + path.to_string() + } else { + std::path::Path::new(root) + .join(path) + .to_string_lossy() + .replace('\\', "/") + } +} + +/// 构造 LSP `file://` URI,保留 `/`,其余字节做百分号编码。 +pub fn file_uri(root: &str, path: &str) -> String { + let absolute = absolute_path(root, path); + let mut encoded = String::with_capacity(absolute.len() + 8); + for byte in absolute.bytes() { + match byte { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' | b'/' => { + encoded.push(byte as char) + } + other => encoded.push_str(&format!("%{other:02X}")), + } + } + format!("file://{encoded}") +} + +/// 把 LSP `file://` URI 映射回工作区相对路径;工作区外返回 `None`。 +pub fn workspace_relative_path(root: &str, uri: &str) -> Option { + let encoded = uri.strip_prefix("file://")?; + let decoded = percent_decode(encoded); + let root = root.replace('\\', "/"); + let root = root.trim_end_matches('/'); + let decoded = decoded.replace('\\', "/"); + if decoded == root { + return Some(String::new()); + } + decoded + .strip_prefix(&format!("{root}/")) + .map(|value| value.to_string()) +} + +fn percent_decode(value: &str) -> String { + let bytes = value.as_bytes(); + let mut decoded = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'%' && index + 2 < bytes.len() { + let hex = std::str::from_utf8(&bytes[index + 1..index + 3]).ok(); + if let Some(byte) = hex.and_then(|value| u8::from_str_radix(value, 16).ok()) { + decoded.push(byte); + index += 3; + continue; + } + } + decoded.push(bytes[index]); + index += 1; + } + String::from_utf8_lossy(&decoded).into_owned() +} + +/// 当前语言服务器可用性;不可用时返回空。 +pub fn resolve_provider(path: &str) -> Option<(LanguageProvider, String)> { + let provider = provider_for_path(path)?; + let executable = find_in_path(provider.executable)?; + Some((provider, executable)) +} + +/// `lsp.startServer` 的最小有效载荷。平台只提供进程与工作区信息, +/// 初始化参数与工作区指纹由 Core 的 provider 适配层负责。 +pub fn start_payload( + provider: LanguageProvider, + executable: &str, + root: &str, + cache_directory: &str, +) -> Value { + let arguments: Vec = provider + .arguments + .iter() + .map(|value| value.to_string()) + .collect(); + json!({ + "providerId": provider.id, + "executablePath": executable, + "arguments": arguments, + "rootUri": file_uri(root, root), + "workingDirectory": absolute_path(root, root), + "initializeTimeoutMilliseconds": 30_000, + "requestTimeoutMilliseconds": 30_000, + "shutdownTimeoutMilliseconds": 2_000, + "cacheDirectory": cache_directory, + }) +} + +pub fn parse_session_id(value: &Value) -> Option { + value + .get("sessionId") + .and_then(Value::as_str) + .map(str::to_string) +} + +pub fn sync_payload(session_id: &str, uri: &str, language_id: &str, text: &str) -> Value { + json!({ + "sessionId": session_id, + "uri": uri, + "languageId": language_id, + "text": text, + }) +} + +pub fn poll_payload(session_id: &str) -> Value { + json!({ "sessionId": session_id }) +} + +/// 语言服务器退出后 Core 只允许在收到终态事件后销毁会话。 +#[allow(dead_code)] +pub fn session_finished(value: &Value) -> bool { + value + .get("events") + .and_then(Value::as_array) + .map(|events| { + events.iter().any(|event| { + let kind = event.get("type").and_then(Value::as_str); + let state = event.get("state").and_then(Value::as_str); + matches!(kind, Some("stateChanged")) && matches!(state, Some("stopped" | "failed")) + }) + }) + .unwrap_or(false) +} + +/// 只接受指定 session 的终态事件,避免混合 fixture 或旧 session 结束新 session。 +pub fn session_finished_for(value: &Value, session_id: &str) -> bool { + value + .get("events") + .and_then(Value::as_array) + .map(|events| { + events.iter().any(|event| { + event.get("sessionId").and_then(Value::as_str) == Some(session_id) + && event.get("type").and_then(Value::as_str) == Some("stateChanged") + && matches!( + event.get("state").and_then(Value::as_str), + Some("stopped" | "failed") + ) + }) + }) + .unwrap_or(false) +} + +/// 把一次 poll 的诊断事件投影为面板条目;无 URI 或工作区外的事件被丢弃。 +pub fn diagnostics_from_poll(value: &Value, root: &str) -> HashMap> { + let mut by_file: HashMap> = HashMap::new(); + let Some(events) = value.get("events").and_then(Value::as_array) else { + return by_file; + }; + for event in events { + if event.get("type").and_then(Value::as_str) != Some("diagnostics") { + continue; + } + let Some(uri) = event.get("uri").and_then(Value::as_str) else { + continue; + }; + let Some(path) = workspace_relative_path(root, uri) else { + continue; + }; + let diagnostics = event + .get("diagnostics") + .and_then(Value::as_array) + .map(|values| { + values + .iter() + .map(|diagnostic| DiagnosticEntry { + severity: severity_name(diagnostic.get("severity").and_then(Value::as_i64)), + file_path: path.clone(), + line: diagnostic + .pointer("/range/start/line") + .and_then(Value::as_u64) + .unwrap_or(0) as u32, + column: diagnostic + .pointer("/range/start/utf16Column") + .and_then(Value::as_u64) + .unwrap_or(0) as u32, + message: diagnostic + .get("message") + .and_then(Value::as_str) + .unwrap_or_default() + .to_string(), + source: diagnostic + .get("source") + .and_then(Value::as_str) + .map(str::to_string), + code: diagnostic + .get("code") + .and_then(Value::as_str) + .map(str::to_string), + }) + .collect::>() + }) + .unwrap_or_default(); + by_file.insert(path, diagnostics); + } + by_file +} + +/// 只投影指定 session 的诊断事件。 +pub fn diagnostics_from_poll_for( + value: &Value, + root: &str, + session_id: &str, +) -> HashMap> { + let mut filtered = value.clone(); + if let Some(events) = filtered.get_mut("events").and_then(Value::as_array_mut) { + events.retain(|event| event.get("sessionId").and_then(Value::as_str) == Some(session_id)); + } + diagnostics_from_poll(&filtered, root) +} + +fn severity_name(severity: Option) -> String { + match severity { + Some(1) => "error".to_string(), + Some(2) => "warning".to_string(), + Some(3) => "info".to_string(), + Some(4) => "hint".to_string(), + _ => "info".to_string(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encodes_and_decodes_file_uris() { + let uri = file_uri("/work space", "src/主 file.rs"); + assert_eq!(uri, "file:///work%20space/src/%E4%B8%BB%20file.rs"); + assert_eq!( + workspace_relative_path("/work space", &uri).as_deref(), + Some("src/主 file.rs") + ); + } + + #[test] + fn selects_provider_by_extension() { + assert_eq!( + provider_for_path("src/main.rs").map(|p| p.id), + Some("rust-analyzer") + ); + assert_eq!( + provider_for_path("src/App.tsx").map(|p| p.id), + Some("typescript-language-server") + ); + assert_eq!( + provider_for_path("src/App.java").map(|p| p.id), + Some("java") + ); + assert_eq!(provider_for_path("README.md").map(|p| p.id), None); + } + + #[test] + fn refines_language_id_for_shared_providers() { + let clangd = provider_for_path("src/native.cpp").expect("clangd"); + assert_eq!(language_id_for_path("src/native.cpp", clangd), "cpp"); + assert_eq!(language_id_for_path("src/native.c", clangd), "c"); + let tsserver = provider_for_path("src/app.js").expect("tsserver"); + assert_eq!(language_id_for_path("src/app.js", tsserver), "javascript"); + assert_eq!(language_id_for_path("src/app.ts", tsserver), "typescript"); + } + + #[test] + fn projects_diagnostics_inside_the_workspace() { + let value = serde_json::json!({ + "events": [{ + "type": "diagnostics", + "uri": "file:///work/src/main.rs", + "diagnostics": [{ + "severity": 1, + "message": "mismatched types", + "range": { "start": { "line": 4, "utf16Column": 7 } } + }] + }] + }); + let diagnostics = diagnostics_from_poll(&value, "/work"); + let entries = diagnostics.get("src/main.rs").expect("diagnostics"); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].severity, "error"); + assert_eq!(entries[0].line, 4); + assert_eq!(entries[0].column, 7); + } + + fn fixture(name: &str) -> PathBuf { + let path = std::env::temp_dir().join(format!( + "lithe-linux-lsp-{name}-{}-{}", + std::process::id(), + name.len() + )); + let _ = fs::remove_dir_all(&path); + fs::create_dir_all(&path).expect("fixture"); + path + } + + fn direct_jdtls_fixture(root: &Path) -> JdtlsLaunchResources { + let plugins = root.join("plugins"); + let debug = root.join("java-debug"); + let extensions = root.join("java-test").join("extensions"); + fs::create_dir_all(&plugins).unwrap(); + fs::create_dir_all(root.join(linux_configuration_name())).unwrap(); + fs::create_dir_all(root.join("lombok")).unwrap(); + fs::create_dir_all(&debug).unwrap(); + fs::create_dir_all(&extensions).unwrap(); + fs::write(plugins.join("org.eclipse.equinox.launcher_2.jar"), b"jar").unwrap(); + fs::write(plugins.join("org.eclipse.equinox.launcher_1.jar"), b"jar").unwrap(); + fs::write(root.join("lombok").join("lombok.jar"), b"jar").unwrap(); + fs::write(debug.join("com.microsoft.java.debug.plugin-2.jar"), b"jar").unwrap(); + fs::write(debug.join("com.microsoft.java.debug.plugin-1.jar"), b"jar").unwrap(); + fs::write(extensions.join("z.jar"), b"jar").unwrap(); + fs::write(extensions.join("a.jar"), b"jar").unwrap(); + fs::write( + root.join("java-test").join("extensions.txt"), + "z.jar\na.jar\n", + ) + .unwrap(); + resolve_direct_jdtls_resources(root).expect("direct resources") + } + + #[test] + fn direct_resources_are_sorted_and_payload_uses_java_provider() { + let root = fixture("resources"); + let jdtls = root.join("jdtls"); + let jdk = root.join("jdk"); + fs::create_dir_all(jdk.join("bin")).unwrap(); + fs::write(jdk.join("bin").join("java"), b"java").unwrap(); + let resources = direct_jdtls_fixture(&jdtls); + assert!(resources + .launcher_jar_path + .ends_with("org.eclipse.equinox.launcher_1.jar")); + assert!(resources + .java_debug_bundle_path + .ends_with("com.microsoft.java.debug.plugin-1.jar")); + assert_eq!( + resources.java_extension_bundle_paths, + vec![ + path_string(&jdtls.join("java-test/extensions/a.jar")), + path_string(&jdtls.join("java-test/extensions/z.jar")), + ] + ); + let launch = JavaLspLaunch { + provider_id: JAVA_PROVIDER_ID.to_string(), + language_id: JAVA_PROVIDER_ID.to_string(), + executable_path: path_string(&jdk.join("bin/java")), + arguments: Vec::new(), + runtime_executable_path: Some(path_string(&jdk.join("bin/java"))), + jdtls_launch_resources: Some(resources), + java_home_path: Some(path_string(&jdk)), + jdtls_version: "1.61.0".to_string(), + }; + let payload = java_start_payload( + &launch, + &root.to_string_lossy(), + "/tmp/lithe-jdt-cache", + Some("build=fingerprint"), + Some(&json!({"version": 1, "reactorPath": "."})), + ); + assert_eq!(payload["providerId"], "java"); + assert_eq!( + payload["runtimeExecutablePath"], + path_string(&jdk.join("bin/java")) + ); + assert_eq!( + payload["jdtlsLaunchResources"]["launcherJarPath"], + path_string(&jdtls.join("plugins/org.eclipse.equinox.launcher_1.jar")) + ); + assert_eq!( + payload["jdtlsLaunchResources"]["configurationDirectory"], + path_string(&jdtls.join(linux_configuration_name())) + ); + assert_eq!( + payload["jdtlsLaunchResources"]["lombokAgentPath"], + path_string(&jdtls.join("lombok/lombok.jar")) + ); + assert_eq!( + payload["jdtlsLaunchResources"]["javaDebugBundlePath"], + path_string(&jdtls.join("java-debug/com.microsoft.java.debug.plugin-1.jar")) + ); + assert_eq!( + payload["jdtlsLaunchResources"]["javaExtensionBundlePaths"], + serde_json::json!([ + path_string(&jdtls.join("java-test/extensions/a.jar")), + path_string(&jdtls.join("java-test/extensions/z.jar")) + ]) + ); + assert_eq!(payload["workspaceFingerprint"], "build=fingerprint"); + assert_eq!(payload["mavenContext"]["reactorPath"], "."); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn missing_direct_resources_return_an_actionable_error() { + let root = fixture("missing-resources"); + let jdtls = root.join("jdtls"); + fs::create_dir_all(&jdtls).unwrap(); + let error = resolve_direct_jdtls_resources(&jdtls).expect_err("missing launcher"); + assert!(error.contains("launcher") && error.contains("jdtls")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn parses_java_launch_results_without_building_a_classpath() { + let entrypoints = parse_java_entrypoints(&json!({ + "schemaVersion": 1, + "entries": [ + {"sourcePath": "src/App.java", "mainClass": "demo.App", "projectName": "app"}, + {"sourcePath": "src/Other.java", "mainClass": "demo.Other"} + ] + })) + .expect("entrypoints"); + let selected = select_java_entrypoint(&entrypoints, "src/App.java", Some("demo.App")) + .expect("selected"); + let launch = java_launch_payload( + &selected, + &json!({"value": [[ "modules/app" ], ["classes/app", "classes/lib"]]}), + ) + .expect("launch"); + assert_eq!(launch["mainClass"], "demo.App"); + assert_eq!(launch["projectName"], "app"); + assert_eq!(launch["classPaths"][1], "classes/lib"); + assert!(launch.get("classpath").is_none()); + } + + #[test] + fn parses_java_build_result_and_rejects_non_success_status() { + assert!(parse_java_build_result(&json!({"value": 1})).is_ok()); + assert!(parse_java_build_result(&json!({"value": 2})).is_err()); + } + + #[test] + fn java_run_gate_requires_core_project_preparation_ready() { + let mut status = JavaLspStatus { + session_id: Some("java-1".to_string()), + state: "ready".to_string(), + error: None, + project_preparation: Some(json!({"status": "loading", "blocksRun": true})), + generation: 1, + }; + assert!(!java_lsp_allows_run(&status)); + status.project_preparation = Some(json!({"status": "ready", "blocksRun": false})); + assert!(java_lsp_allows_run(&status)); + status.project_preparation = Some(json!({"status": "ready", "blocksRun": true})); + assert!(!java_lsp_allows_run(&status)); + status.project_preparation = Some(json!({"status": "failed", "blocksRun": false})); + assert!(java_lsp_allows_run(&status)); + } + + #[test] + fn operation_results_reject_events_from_another_session() { + let value = json!({ + "events": [ + { + "type": "requestCompleted", + "sessionId": "old", + "operationId": "op-old", + "result": {"value": 1} + }, + { + "type": "requestCompleted", + "sessionId": "current", + "operationId": "op-current", + "error": { + "code": "javaBuildCompilationErrors", + "message": "compilation failed", + "details": "markerScope=launchTarget", + "javaBuildReport": {"recovery": "none"} + } + } + ] + }); + let results = operation_results_from_poll(&value, "current"); + assert_eq!(results.len(), 1); + assert_eq!(results[0].0, "op-current"); + assert!(results[0] + .1 + .error + .as_deref() + .unwrap() + .contains("javaBuildCompilationErrors")); + } +} diff --git a/linux/src/main.rs b/linux/src/main.rs new file mode 100644 index 000000000..ea953fb16 --- /dev/null +++ b/linux/src/main.rs @@ -0,0 +1,48 @@ +//! Linux 工作台入口:初始化设置与主题,打开主窗口。 + +mod core; +mod i18n; +mod keybindings; +mod lsp; +mod settings; +mod theme; +mod workbench; + +use gpui_kit::component::{Root, Theme, ThemeMode}; +use gpui_kit::*; +use workbench::WorkbenchView; + +fn main() { + // 工作台使用大量 Lucide 图标,默认 `Assets` 仅内嵌 101 个,需用全量目录。 + let app = gpui_kit::application().with_assets(gpui_kit::assets::AllAssets); + + app.run(move |cx| { + gpui_kit::init(cx); + + // 先加载持久化设置,再据此决定 gpui-component 主题与工作台调色板。 + settings::init(cx); + let is_dark = { + let settings = settings::get(cx); + let theme_id = settings::resolved_theme_id(settings, false); + !crate::theme::ThemePalette::is_light(&theme_id) + }; + let theme_id = settings::resolved_theme_id(settings::get(cx), false); + settings::apply_theme(&theme_id); + + Theme::change( + if is_dark { + ThemeMode::Dark + } else { + ThemeMode::Light + }, + None, + cx, + ); + + cx.open_window(WindowOptions::default(), |window, cx| { + let view = cx.new(|cx| WorkbenchView::new(window, cx)); + cx.new(|cx| Root::new(view, window, cx)) + }) + .expect("Failed to open window"); + }); +} diff --git a/linux/src/settings.rs b/linux/src/settings.rs new file mode 100644 index 000000000..a3b9405c6 --- /dev/null +++ b/linux/src/settings.rs @@ -0,0 +1,503 @@ +//! 应用设置的 XDG 持久化与运行时全局状态。 +//! +//! 对齐 Tauri 端 `settings.json` 的键名与默认值,落到 +//! `$XDG_CONFIG_HOME/lithe/settings.json`(缺省 `~/.config/lithe/settings.json`)。 +//! 视图通过 [`get`] / [`update`] 读取和修改,修改后写盘并刷新窗口。 + +use std::path::PathBuf; + +use gpui_kit::{App, Global}; +use serde::{Deserialize, Serialize}; + +use crate::theme::ThemePalette; + +/// 活动栏默认项顺序,对应 Tauri `SIDEBAR_ACTIVITY_ITEM_IDS`。 +pub const SIDEBAR_ACTIVITY_ITEM_IDS: &[&str] = &[ + "files", + "git", + "search", + "maven", + "run", + "terminal", + "diagnostics", + "gitLog", + "settings", +]; + +/// 活动栏底部固定组,对应 Tauri `SIDEBAR_BOTTOM_ACTIVITY_ITEM_IDS`。 +pub const SIDEBAR_BOTTOM_ACTIVITY_ITEM_IDS: &[&str] = &[ + "maven", + "run", + "terminal", + "diagnostics", + "gitLog", + "settings", +]; + +/// 状态栏左侧项顺序,对应 Tauri `FOOTER_LEADING_ITEM_IDS`。 +pub const FOOTER_LEADING_ITEM_IDS: &[&str] = &["filePath", "branch"]; + +/// 最近项目上限,对齐 Tauri `MAX_RECENT_PROJECTS`(`recent-folders.ts`)。 +pub const MAX_RECENT_PROJECTS: usize = 12; + +/// 取路径末段目录名用于展示(兼容 `/` 与 `\` 分隔)。 +pub fn project_dir_name(path: &str) -> &str { + path.rsplit(&['/', '\\'][..]) + .next() + .filter(|s| !s.is_empty()) + .unwrap_or(path) +} + +/// 状态栏右侧项顺序,对应 Tauri `FOOTER_TRAILING_ITEM_IDS`。 +pub const FOOTER_TRAILING_ITEM_IDS: &[&str] = &[ + "cursor", + "encoding", + "indent", + "readOnly", + "memory", + "gitChanges", +]; + +/// 功能开关,对应 Tauri `coreFeatures`。 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct CoreFeatures { + pub git: bool, + pub github: bool, + pub remote: bool, + pub terminal: bool, + pub search: bool, + pub diagnostics: bool, + pub debugger: bool, + pub docker: bool, + pub outline: bool, + pub ai_chat: bool, + pub breadcrumbs: bool, + pub persistent_commands: bool, + pub web_viewer: bool, +} + +impl Default for CoreFeatures { + fn default() -> Self { + Self { + git: true, + github: false, + remote: false, + terminal: true, + search: true, + diagnostics: true, + debugger: false, + docker: false, + outline: true, + ai_chat: false, + breadcrumbs: true, + persistent_commands: true, + web_viewer: false, + } + } +} + +/// Tauri 默认隐藏目录模式(`DEFAULT_HIDDEN_DIRECTORY_PATTERNS`)。 +pub const DEFAULT_HIDDEN_DIRECTORY_PATTERNS: &[&str] = &[ + ".git", + ".hg", + ".idea", + ".mypy_cache", + ".pytest_cache", + ".ruff_cache", + ".svn", + "CVS", + "__pycache__", + "_svn", +]; + +/// Tauri 默认隐藏文件模式(`DEFAULT_HIDDEN_FILE_PATTERNS`)。 +pub const DEFAULT_HIDDEN_FILE_PATTERNS: &[&str] = &[ + "*.pyc", + "*.pyo", + "*.rbc", + "*.yarb", + "*~", + ".DS_Store", + "vssver.scc", + "vssver2.scc", +]; + +/// AI 提交信息生成设置,对应 Tauri `CommitAISettings` 的持久化子集。 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct AiCommitSettings { + pub enabled: bool, + pub language: String, + pub format: String, + pub include_body: bool, + pub subject_max_length: usize, + pub maximum_diff_characters: usize, +} + +impl Default for AiCommitSettings { + fn default() -> Self { + Self { + enabled: true, + language: "english".to_string(), + format: "conventional".to_string(), + include_body: false, + subject_max_length: 72, + maximum_diff_characters: 32000, + } + } +} + +/// 将显示语言值规范化为设置下拉框使用的稳定标识。 +pub fn normalized_display_language(value: &str) -> String { + let value = value.trim().to_ascii_lowercase().replace('_', "-"); + if value.starts_with("zh") { + "zh-CN".to_string() + } else if value.starts_with("en") { + "en-US".to_string() + } else { + "zh-CN".to_string() + } +} + +/// 应用设置真源,字段名与 Tauri `defaultSettings` 的 camelCase 键一一对应。 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct Settings { + // General + pub auto_save: bool, + pub quick_open_preview: bool, + // Editor + pub font_family: String, + pub font_size: f32, + pub editor_line_height: f32, + pub tab_size: usize, + pub word_wrap: bool, + pub line_numbers: bool, + pub render_whitespace: String, + pub render_indent_guides: bool, + pub highlight_occurrences: bool, + pub show_minimap: bool, + pub code_lens: bool, + // Terminal + pub terminal_font_size: f32, + pub terminal_cursor_blink: bool, + pub terminal_scrollback: usize, + pub terminal_default_shell_id: String, + // Run + pub run_scroll_to_end: bool, + // UI + pub ui_font_size: f32, + pub display_language: String, + pub reduce_motion: bool, + pub show_status_bar: bool, + pub show_tab_icons: bool, + pub tab_close_button_visibility: String, + pub window_chrome_density: String, + // Theme + pub theme: String, + pub icon_theme: String, + pub sync_system_theme: bool, + pub auto_theme_light: String, + pub auto_theme_dark: String, + pub compact_menu_bar: bool, + // Layout + pub sidebar_activity_items_order: Vec, + pub hidden_sidebar_activity_items: Vec, + pub footer_leading_items_order: Vec, + pub footer_trailing_items_order: Vec, + pub activity_rail_expanded: bool, + pub activity_rail_width: f32, + pub sidebar_width: f32, + pub right_tool_window_width: f32, + // Tabs + pub max_open_tabs: usize, + pub horizontal_tab_scroll: bool, + // Keyboard + pub keybinding_preset: String, + pub vim_mode: bool, + // Language + pub format_on_save: bool, + pub auto_completion: bool, + pub parameter_hints: bool, + pub semantic_tokens: bool, + // Projects + pub open_folders_in_new_window: bool, + pub ask_where_to_open_projects: bool, + /// 最近打开的项目绝对路径,首位最新。 + /// + /// 对齐 Tauri `recent-folders.ts`:去重(已有移到首位)、按打开倒序、 + /// 过滤不存在的路径、上限 [`MAX_RECENT_PROJECTS`](Linux 无 pin,只做倒序)。 + /// 结构体级 `#[serde(default)]` 已覆盖,兼容缺少该键的老配置。 + pub recent_projects: Vec, + // File tree + pub hidden_file_patterns: Vec, + pub hidden_directory_patterns: Vec, + pub file_tree_sort_order: String, + pub file_tree_indent_size: f32, + pub compact_folders_in_file_tree: bool, + pub hide_root_folder_in_file_tree: bool, + pub auto_reveal_active_file_in_file_tree: bool, + pub show_file_icons_in_file_tree: bool, + pub show_indent_guides_in_file_tree: bool, + pub confirm_before_file_delete: bool, + pub show_hidden_files_in_file_tree: bool, + pub show_gitignored_files_in_file_tree: bool, + pub show_git_status_in_file_tree: bool, + // Git + pub auto_refresh_git_status: bool, + pub show_untracked_files: bool, + pub show_staged_first: bool, + pub git_default_diff_view: String, + pub open_diff_on_click: bool, + pub git_changes_folder_view: bool, + pub compact_git_status_badges: bool, + pub collapse_empty_git_sections: bool, + pub remember_last_git_panel_mode: bool, + pub confirm_before_discard: bool, + pub enable_inline_git_blame: bool, + pub git_fetch_prune: bool, + pub git_fetch_submodules: String, + pub git_fetch_tags: String, + pub git_executable: String, + pub git_use_credential_helper: bool, + // AI + pub ai_provider_id: String, + pub ai_model_id: String, + pub ai_completion: bool, + pub ai_commit: AiCommitSettings, + // Logs + pub custom_log_directory: String, + // Advanced + pub last_settings_tab: String, + pub core_features: CoreFeatures, +} + +impl Default for Settings { + fn default() -> Self { + Self { + auto_save: true, + quick_open_preview: true, + font_family: "Geist Mono".to_string(), + font_size: 14.0, + editor_line_height: 1.4, + tab_size: 2, + word_wrap: false, + line_numbers: true, + render_whitespace: "none".to_string(), + render_indent_guides: true, + highlight_occurrences: true, + show_minimap: true, + code_lens: true, + terminal_font_size: 14.0, + terminal_cursor_blink: true, + terminal_scrollback: 10000, + terminal_default_shell_id: String::new(), + run_scroll_to_end: true, + ui_font_size: 13.0, + display_language: normalized_display_language("zh-CN"), + reduce_motion: false, + show_status_bar: true, + show_tab_icons: true, + tab_close_button_visibility: "active".to_string(), + window_chrome_density: "focused".to_string(), + theme: "lithe-dark".to_string(), + icon_theme: "idea-icons".to_string(), + sync_system_theme: false, + auto_theme_light: "lithe-light".to_string(), + auto_theme_dark: "lithe-dark".to_string(), + compact_menu_bar: true, + sidebar_activity_items_order: SIDEBAR_ACTIVITY_ITEM_IDS + .iter() + .map(|s| s.to_string()) + .collect(), + hidden_sidebar_activity_items: Vec::new(), + footer_leading_items_order: FOOTER_LEADING_ITEM_IDS + .iter() + .map(|s| s.to_string()) + .collect(), + footer_trailing_items_order: FOOTER_TRAILING_ITEM_IDS + .iter() + .map(|s| s.to_string()) + .collect(), + activity_rail_expanded: false, + activity_rail_width: 180.0, + sidebar_width: 320.0, + right_tool_window_width: 400.0, + max_open_tabs: 100, + horizontal_tab_scroll: true, + keybinding_preset: "none".to_string(), + vim_mode: false, + format_on_save: false, + auto_completion: true, + parameter_hints: true, + semantic_tokens: true, + open_folders_in_new_window: true, + ask_where_to_open_projects: true, + recent_projects: Vec::new(), + hidden_file_patterns: DEFAULT_HIDDEN_FILE_PATTERNS + .iter() + .map(|s| s.to_string()) + .collect(), + hidden_directory_patterns: DEFAULT_HIDDEN_DIRECTORY_PATTERNS + .iter() + .map(|s| s.to_string()) + .collect(), + file_tree_sort_order: "folders-first".to_string(), + file_tree_indent_size: 16.0, + compact_folders_in_file_tree: true, + hide_root_folder_in_file_tree: false, + auto_reveal_active_file_in_file_tree: true, + show_file_icons_in_file_tree: true, + show_indent_guides_in_file_tree: true, + confirm_before_file_delete: true, + show_hidden_files_in_file_tree: true, + show_gitignored_files_in_file_tree: true, + show_git_status_in_file_tree: true, + auto_refresh_git_status: true, + show_untracked_files: true, + show_staged_first: true, + git_default_diff_view: "unified".to_string(), + open_diff_on_click: true, + git_changes_folder_view: true, + compact_git_status_badges: false, + collapse_empty_git_sections: false, + remember_last_git_panel_mode: false, + confirm_before_discard: true, + enable_inline_git_blame: true, + git_fetch_prune: true, + git_fetch_submodules: "inherit".to_string(), + git_fetch_tags: "inherit".to_string(), + git_executable: String::new(), + git_use_credential_helper: true, + ai_provider_id: "anthropic".to_string(), + ai_model_id: "claude-sonnet-4-6".to_string(), + ai_completion: true, + ai_commit: AiCommitSettings::default(), + custom_log_directory: String::new(), + last_settings_tab: "general".to_string(), + core_features: CoreFeatures::default(), + } + } +} + +/// 全局设置持有者,注册到 GPUI 的 `Global` 存储。 +pub struct AppSettings { + pub settings: Settings, +} + +impl Global for AppSettings {} + +/// 当前设置(只读)。 +pub fn get(cx: &App) -> &Settings { + &cx.global::().settings +} + +/// 当前设置(可变),修改后需调用 [`persist`]。 +pub fn get_mut(cx: &mut App) -> &mut Settings { + &mut cx.global_mut::().settings +} + +/// 解析配置文件路径;优先 `XDG_CONFIG_HOME`,否则 `~/.config`。 +pub fn config_path() -> Option { + let base = std::env::var_os("XDG_CONFIG_HOME") + .map(PathBuf::from) + .filter(|p| !p.as_os_str().is_empty()) + .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))?; + Some(base.join("lithe").join("settings.json")) +} + +/// 从磁盘加载设置;缺失或损坏时回退到默认值并补齐顺序字段。 +pub fn load() -> Settings { + let Some(path) = config_path() else { + return Settings::default(); + }; + let Ok(text) = std::fs::read_to_string(&path) else { + return Settings::default(); + }; + let mut settings = serde_json::from_str::(&text).unwrap_or_default(); + settings.display_language = normalized_display_language(&settings.display_language); + settings +} + +/// 将设置写回磁盘(临时文件 + 原子替换)。 +pub fn persist(settings: &Settings) { + let Some(path) = config_path() else { + return; + }; + if let Some(parent) = path.parent() { + if std::fs::create_dir_all(parent).is_err() { + return; + } + } + let Ok(text) = serde_json::to_string_pretty(settings) else { + return; + }; + let tmp = path.with_extension("json.tmp"); + if std::fs::write(&tmp, text).is_ok() { + let _ = std::fs::rename(&tmp, &path); + } +} + +/// 初始化全局设置(应用启动时调用一次)。 +pub fn init(cx: &mut App) { + let settings = load(); + cx.set_global(AppSettings { settings }); +} + +/// 修改设置:写全局、落盘并刷新所有窗口。 +pub fn update(cx: &mut App, f: impl FnOnce(&mut Settings)) { + f(get_mut(cx)); + let settings = get_mut(cx); + settings.display_language = normalized_display_language(&settings.display_language); + let snapshot = get(cx).clone(); + persist(&snapshot); + cx.refresh_windows(); +} + +/// 记录最近项目(对齐 Tauri upsert):去重移到首位 → 过滤不存在 → 截断上限 → 落盘。 +pub fn record_recent_project(cx: &mut App, path: &str) { + update(cx, |s| { + s.recent_projects.retain(|p| p != path); + s.recent_projects.insert(0, path.to_string()); + s.recent_projects + .retain(|p| std::path::Path::new(p).exists()); + s.recent_projects.truncate(MAX_RECENT_PROJECTS); + }); +} + +/// 解析当前应生效的主题 id:跟随系统时按外观取 `autoTheme*`,否则取 `theme`。 +pub fn resolved_theme_id(settings: &Settings, system_is_dark: bool) -> String { + if settings.sync_system_theme { + if system_is_dark { + settings.auto_theme_dark.clone() + } else { + settings.auto_theme_light.clone() + } + } else { + settings.theme.clone() + } +} + +/// 把主题 id 应用到工作台调色板;未知 id 回退深色。 +pub fn apply_theme(theme_id: &str) { + let palette = ThemePalette::for_theme_id(theme_id).unwrap_or_else(ThemePalette::dark); + crate::theme::set_palette(palette); +} + +#[cfg(test)] +mod tests { + use super::normalized_display_language; + + #[test] + fn display_language_aliases_match_settings_options() { + assert_eq!(normalized_display_language("zh"), "zh-CN"); + assert_eq!(normalized_display_language("zh_CN"), "zh-CN"); + assert_eq!(normalized_display_language("en_US"), "en-US"); + } + + #[test] + fn unknown_display_language_uses_product_default() { + assert_eq!(normalized_display_language("fr-FR"), "zh-CN"); + } +} diff --git a/linux/src/theme.rs b/linux/src/theme.rs new file mode 100644 index 000000000..67fe1974e --- /dev/null +++ b/linux/src/theme.rs @@ -0,0 +1,363 @@ +//! 工作台配色真源:对齐 Tauri 端 `lithe-dark` / `lithe-light` 主题 token。 +//! +//! 调色板在运行时通过 [`set_palette`] 切换,[`ThemeColors`] 的访问器读取当前 +//! 调色板,因此所有视图无需持有 `Context` 即可跟随主题变化。 + +use std::sync::RwLock; + +use gpui_kit::Rgba; + +#[allow(unused_imports)] +pub use gpui_kit::assets::IconName; +#[allow(unused_imports)] +pub use gpui_kit::component::Icon; + +/// 由 `0xRRGGBB` 构造不透明颜色。 +pub const fn const_rgb(hex: u32) -> Rgba { + const_rgba(hex, 1.0) +} + +/// 由 `0xRRGGBB` 与 alpha 构造颜色。 +pub const fn const_rgba(hex: u32, a: f32) -> Rgba { + let r = ((hex >> 16) & 0xFF) as f32 / 255.0; + let g = ((hex >> 8) & 0xFF) as f32 / 255.0; + let b = (hex & 0xFF) as f32 / 255.0; + Rgba { r, g, b, a } +} + +/// 在两个颜色之间按 `t`(0..=1)线性插值,保留 `a` 的 alpha。 +pub fn mix(from: Rgba, to: Rgba, t: f32) -> Rgba { + let t = t.clamp(0.0, 1.0); + Rgba { + r: from.r + (to.r - from.r) * t, + g: from.g + (to.g - from.g) * t, + b: from.b + (to.b - from.b) * t, + a: from.a, + } +} + +/// 一套完整的工作台配色,字段名与 Tauri 主题 token 对应。 +/// +/// 部分 token(git/syntax/terminal 等)当前视图尚未消费,但作为主题真源保留, +/// 供后续编辑器高亮与终端渲染直接取用。 +#[derive(Debug, Clone, Copy)] +#[allow(dead_code)] +pub struct ThemePalette { + pub background: Rgba, + pub surface: Rgba, + pub foreground: Rgba, + pub muted_foreground: Rgba, + pub subtle_foreground: Rgba, + pub border: Rgba, + pub accent: Rgba, + pub selected: Rgba, + pub selection: Rgba, + pub primary: Rgba, + pub destructive: Rgba, + pub success: Rgba, + pub warning: Rgba, + pub info: Rgba, + pub git_modified: Rgba, + pub git_added: Rgba, + pub git_deleted: Rgba, + pub git_untracked: Rgba, + pub git_renamed: Rgba, + pub terminal_black: Rgba, + pub terminal_red: Rgba, + pub terminal_green: Rgba, + pub terminal_yellow: Rgba, + pub terminal_blue: Rgba, + pub terminal_magenta: Rgba, + pub terminal_cyan: Rgba, + pub terminal_white: Rgba, + pub syntax_comment: Rgba, + pub syntax_keyword: Rgba, + pub syntax_string: Rgba, + pub syntax_number: Rgba, + pub syntax_function: Rgba, + pub syntax_variable: Rgba, + pub syntax_type: Rgba, + pub syntax_constant: Rgba, + pub syntax_property: Rgba, +} + +impl ThemePalette { + /// `lithe-dark`:分层中性表面与 Lithe 蓝强调色。 + pub const fn dark() -> Self { + Self { + background: const_rgb(0x1e1f22), + surface: const_rgb(0x2b2d30), + foreground: const_rgb(0xdfe1e5), + muted_foreground: const_rgb(0xb4b8bf), + subtle_foreground: const_rgb(0x8b929e), + border: const_rgb(0x43454a), + accent: const_rgb(0x393b40), + selected: const_rgb(0x2e436e), + selection: const_rgba(0x214283, 1.0), + primary: const_rgb(0x3574f0), + destructive: const_rgb(0xdb5c5c), + success: const_rgb(0x57965c), + warning: const_rgb(0xd6ae58), + info: const_rgb(0x548af7), + git_modified: const_rgb(0xd9a441), + git_added: const_rgb(0x4cc38a), + git_deleted: const_rgb(0xf16d75), + git_untracked: const_rgb(0x58a6e7), + git_renamed: const_rgb(0xc8a2f4), + terminal_black: const_rgb(0x0f1012), + terminal_red: const_rgb(0xf16d75), + terminal_green: const_rgb(0x4cc38a), + terminal_yellow: const_rgb(0xd9a441), + terminal_blue: const_rgb(0x58a6e7), + terminal_magenta: const_rgb(0xc8a2f4), + terminal_cyan: const_rgb(0x61c0bf), + terminal_white: const_rgb(0xc4c9d1), + syntax_comment: const_rgb(0x7a7e85), + syntax_keyword: const_rgb(0xcf8e6d), + syntax_string: const_rgb(0x6aab73), + syntax_number: const_rgb(0x2aacb8), + syntax_function: const_rgb(0x56a8f5), + syntax_variable: const_rgb(0xbcbec4), + syntax_type: const_rgb(0xbcbec4), + syntax_constant: const_rgb(0xc77dbb), + syntax_property: const_rgb(0xc77dbb), + } + } + + /// `lithe-light`:清晰中性表面与高对比文本。 + pub const fn light() -> Self { + Self { + background: const_rgb(0xffffff), + surface: const_rgb(0xf7f8fa), + foreground: const_rgb(0x1f2328), + muted_foreground: const_rgb(0x4f5965), + subtle_foreground: const_rgb(0x68717d), + border: const_rgb(0xdfe1e5), + accent: const_rgb(0xedf3ff), + selected: const_rgb(0xd4e2ff), + selection: const_rgba(0x3574f0, 0.2), + primary: const_rgb(0x3574f0), + destructive: const_rgb(0xcf3f4f), + success: const_rgb(0x27864f), + warning: const_rgb(0xa86400), + info: const_rgb(0x3574f0), + git_modified: const_rgb(0xa86400), + git_added: const_rgb(0x27864f), + git_deleted: const_rgb(0xcf3f4f), + git_untracked: const_rgb(0x0877c1), + git_renamed: const_rgb(0x7656a8), + terminal_black: const_rgb(0x1f2328), + terminal_red: const_rgb(0xcf3f4f), + terminal_green: const_rgb(0x27864f), + terminal_yellow: const_rgb(0xa86400), + terminal_blue: const_rgb(0x0877c1), + terminal_magenta: const_rgb(0x8a4fb0), + terminal_cyan: const_rgb(0x147d83), + terminal_white: const_rgb(0x68717d), + syntax_comment: const_rgb(0x68717d), + syntax_keyword: const_rgb(0xb83280), + syntax_string: const_rgb(0x287d3c), + syntax_number: const_rgb(0xa15c00), + syntax_function: const_rgb(0x14777d), + syntax_variable: const_rgb(0x7656a8), + syntax_type: const_rgb(0x4169a8), + syntax_constant: const_rgb(0xa15c00), + syntax_property: const_rgb(0x075e9e), + } + } + + /// 按主题 id 解析内置调色板;未知 id 返回 `None`,由调用方回退。 + pub fn for_theme_id(theme_id: &str) -> Option { + match theme_id { + "lithe-light" => Some(Self::light()), + "lithe-dark" => Some(Self::dark()), + _ => None, + } + } + + /// 该主题是否为浅色外观。 + pub fn is_light(theme_id: &str) -> bool { + theme_id == "lithe-light" + } + + /// `border-strong`:边框与前景按 72/28 混合。 + pub fn border_strong(&self) -> Rgba { + mix(self.border, self.foreground, 0.28) + } + + /// `tab-hover-bg`:强调色 72% 叠在背景之上。 + pub fn tab_hover(&self) -> Rgba { + mix(self.background, self.accent, 0.72) + } +} + +static CURRENT_PALETTE: RwLock = RwLock::new(ThemePalette::dark()); + +/// 切换当前工作台调色板;调用方随后需刷新窗口。 +pub fn set_palette(palette: ThemePalette) { + if let Ok(mut current) = CURRENT_PALETTE.write() { + *current = palette; + } +} + +/// 当前生效的工作台调色板。 +pub fn palette() -> ThemePalette { + CURRENT_PALETTE + .read() + .map(|p| *p) + .unwrap_or_else(|_| ThemePalette::dark()) +} + +/// 语义化颜色访问器。旧命名保留为别名,避免视图层大范围重写。 +pub struct ThemeColors; + +#[allow(dead_code)] +impl ThemeColors { + // ---- Tauri token 命名 ---- + #[inline] + pub fn background() -> Rgba { + palette().background + } + #[inline] + pub fn surface() -> Rgba { + palette().surface + } + #[inline] + pub fn foreground() -> Rgba { + palette().foreground + } + #[inline] + pub fn muted_foreground() -> Rgba { + palette().muted_foreground + } + #[inline] + pub fn subtle_foreground() -> Rgba { + palette().subtle_foreground + } + #[inline] + pub fn border() -> Rgba { + palette().border + } + #[inline] + pub fn border_strong() -> Rgba { + palette().border_strong() + } + #[inline] + pub fn accent() -> Rgba { + palette().accent + } + #[inline] + pub fn selected() -> Rgba { + palette().selected + } + #[inline] + pub fn selection() -> Rgba { + palette().selection + } + #[inline] + pub fn primary() -> Rgba { + palette().primary + } + #[inline] + pub fn destructive() -> Rgba { + palette().destructive + } + #[inline] + pub fn success() -> Rgba { + palette().success + } + #[inline] + pub fn warning() -> Rgba { + palette().warning + } + #[inline] + pub fn info() -> Rgba { + palette().info + } + #[inline] + pub fn git_modified() -> Rgba { + palette().git_modified + } + #[inline] + pub fn git_added() -> Rgba { + palette().git_added + } + #[inline] + pub fn git_deleted() -> Rgba { + palette().git_deleted + } + #[inline] + pub fn git_untracked() -> Rgba { + palette().git_untracked + } + #[inline] + pub fn git_renamed() -> Rgba { + palette().git_renamed + } + + // ---- 兼容旧访问器(映射到 Tauri token) ---- + #[inline] + pub fn bg_titlebar() -> Rgba { + palette().background + } + #[inline] + pub fn bg_activity_rail() -> Rgba { + palette().surface + } + #[inline] + pub fn bg_sidebar() -> Rgba { + palette().surface + } + #[inline] + pub fn bg_editor() -> Rgba { + palette().background + } + #[inline] + pub fn bg_tab_bar() -> Rgba { + palette().background + } + #[inline] + pub fn bg_tab_active() -> Rgba { + palette().background + } + #[inline] + pub fn bg_tab_hover() -> Rgba { + palette().tab_hover() + } + #[inline] + pub fn bg_bottom_panel() -> Rgba { + palette().background + } + #[inline] + pub fn bg_statusbar() -> Rgba { + palette().background + } + #[inline] + pub fn accent_blue() -> Rgba { + palette().primary + } + #[inline] + pub fn accent_green() -> Rgba { + palette().success + } + #[inline] + pub fn accent_yellow() -> Rgba { + palette().warning + } + #[inline] + pub fn accent_red() -> Rgba { + palette().destructive + } + #[inline] + pub fn text_primary() -> Rgba { + palette().foreground + } + #[inline] + pub fn text_muted() -> Rgba { + palette().subtle_foreground + } + #[inline] + pub fn subtle_selection() -> Rgba { + palette().accent + } +} diff --git a/linux/src/workbench/activity_rail.rs b/linux/src/workbench/activity_rail.rs new file mode 100644 index 000000000..cd1dfd082 --- /dev/null +++ b/linux/src/workbench/activity_rail.rs @@ -0,0 +1,427 @@ +//! 活动栏:左侧主活动栏 + 右侧插件活动栏,复刻 Tauri `sidebar-pane-selector.tsx` +//! 与 `plugin-activity-rail.tsx`。 +//! +//! 左侧项顺序与可见性完全来自设置(`sidebar_activity_items_order`、 +//! `hidden_sidebar_activity_items`),并按 `core_features` 过滤后端不可用项; +//! 顺序与 Tauri 的 `SIDEBAR_ACTIVITY_ITEM_IDS` / `SIDEBAR_BOTTOM_ACTIVITY_ITEM_IDS` +//! 保持一致。所有交互只通过事件向外广播,具体业务由 `view.rs` 订阅处理。 + +use gpui_kit::assets::IconName; +use gpui_kit::component::button::{Button, ButtonVariants as _}; +use gpui_kit::component::{h_flex, v_flex}; +use gpui_kit::prelude::FluentBuilder as _; +use gpui_kit::{ + div, px, AnyElement, Context, EventEmitter, IntoElement, ParentElement as _, Render, + Styled as _, Window, +}; + +use crate::settings::{ + self, CoreFeatures, SIDEBAR_ACTIVITY_ITEM_IDS, SIDEBAR_BOTTOM_ACTIVITY_ITEM_IDS, +}; +use crate::theme::ThemeColors; + +/// 左侧活动栏事件。`SelectView` 只承载顶部视图类(files/git/search), +/// `ToggleBottomPane` 承载底部工具面板类,二者取值与 Tauri 的稳定 id 一致。 +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub enum ActivityRailEvent { + /// 切换顶部侧边视图,载荷为 "files" | "git" | "search"。 + SelectView(String), + /// 切换底部工具窗口,载荷为 "maven" | "run" | "terminal" | "diagnostics" | "gitLog"。 + ToggleBottomPane(String), + /// 打开设置。 + OpenSettings, +} + +pub struct ActivityRailView { + /// 当前激活的顶部视图 id;`None` 表示侧边栏整体收起。 + pub active_view: Option, + /// 当前激活的底部工具窗口 id;`None` 表示底部面板收起。 + pub active_bottom: Option, + /// 顶部组项顺序(已过滤后端能力与隐藏项)。 + top_items: Vec<&'static str>, + /// 底部组项顺序,按 `SIDEBAR_BOTTOM_ACTIVITY_ITEM_IDS` 固定顺序排列。 + bottom_items: Vec<&'static str>, + /// 是否发生过 Maven 运行。对齐 Tauri:左侧栏的 maven 项仅在 + /// `hasMavenRun` 为真时出现(`onMavenClick` 有条件传入); + /// 右侧插件栏的 Maven 入口不受此限制。 + /// Maven 运行尚未接入后端,当前恒为 false,由后续 Maven 集成调用 + /// [`ActivityRailView::set_has_maven_run`] 打开。 + #[allow(dead_code)] + has_maven_run: bool, +} + +impl EventEmitter for ActivityRailView {} + +impl ActivityRailView { + /// 从设置构建活动栏项列表。 + /// + /// 构造期只做一次快照:设置变更由 `view.rs` 重建/重订阅处理,这里不持有 + /// `App`,避免把全局状态带进渲染路径。 + pub fn new(cx: &mut Context) -> Self { + let (top_items, bottom_items) = Self::compute_items(false, cx); + + Self { + active_view: Some("files".to_string()), + active_bottom: None, + top_items, + bottom_items, + has_maven_run: false, + } + } + + /// 按设置与 `hasMavenRun` 计算左右分组。maven 仅在发生过 Maven 运行后 + /// 才出现在左侧栏,与 Tauri `main-sidebar.tsx` 的条件传入一致。 + fn compute_items( + has_maven_run: bool, + cx: &mut Context, + ) -> (Vec<&'static str>, Vec<&'static str>) { + let s = settings::get(cx); + let ordered = normalize_order(&s.sidebar_activity_items_order); + let features = &s.core_features; + let hidden = &s.hidden_sidebar_activity_items; + + let visible: Vec<&'static str> = ordered + .into_iter() + .filter(|id| is_feature_available(id, features)) + .filter(|id| !hidden.iter().any(|h| h.as_str() == *id)) + .filter(|id| *id != "maven" || has_maven_run) + .collect(); + + // 顶部组保留可见顺序;底部组按 Tauri 常量顺序取交集,保证与参考实现一致。 + let top_items = visible + .iter() + .copied() + .filter(|id| !SIDEBAR_BOTTOM_ACTIVITY_ITEM_IDS.contains(id)) + .collect(); + let bottom_items = SIDEBAR_BOTTOM_ACTIVITY_ITEM_IDS + .iter() + .copied() + .filter(|id| visible.contains(id)) + .collect(); + (top_items, bottom_items) + } + + /// 设置是否发生过 Maven 运行;为真时左侧栏出现 maven 项。 + #[allow(dead_code)] + pub fn set_has_maven_run(&mut self, has_run: bool, cx: &mut Context) { + self.has_maven_run = has_run; + let (top_items, bottom_items) = Self::compute_items(has_run, cx); + self.top_items = top_items; + self.bottom_items = bottom_items; + if !has_run && self.active_bottom.as_deref() == Some("maven") { + self.active_bottom = None; + } + cx.notify(); + } + + pub fn set_active_view(&mut self, view: Option, cx: &mut Context) { + self.active_view = view; + cx.notify(); + } + + pub fn set_active_bottom(&mut self, bottom: Option, cx: &mut Context) { + self.active_bottom = bottom; + cx.notify(); + } + + /// 该 id 对应的事件出口:视图类发 `SelectView`,settings 发 `OpenSettings`, + /// 其余底部项发 `ToggleBottomPane`。 + fn event_for(id: &'static str) -> ActivityRailEvent { + match id { + "files" | "git" | "search" => ActivityRailEvent::SelectView(id.to_string()), + "settings" => ActivityRailEvent::OpenSettings, + other => ActivityRailEvent::ToggleBottomPane(other.to_string()), + } + } + + fn render_item(&self, id: &'static str, cx: &mut Context) -> AnyElement { + let icon = item_icon(id); + let tooltip = item_tooltip(cx, id); + let is_active = + self.active_view.as_deref() == Some(id) || self.active_bottom.as_deref() == Some(id); + let event = Self::event_for(id); + + // 对齐 Tauri `SidebarListItem` iconOnly 激活态(`bg-accent text-foreground`, + // 无左侧竖条):激活态用 accent 背景 + 前景文字,hover 反馈由 ghost 变体内置提供。 + div() + .w(px(32.0)) + .h(px(32.0)) + .flex_shrink_0() + .child( + Button::new(format!("rail-{id}")) + .ghost() + .icon(icon) + .tooltip(tooltip) + .w(px(32.0)) + .h(px(32.0)) + .rounded(px(4.0)) + .text_color(if is_active { + ThemeColors::text_primary() + } else { + ThemeColors::subtle_foreground() + }) + .when(is_active, |btn| btn.bg(ThemeColors::accent())) + .on_click(cx.listener(move |_this, _event, _window, cx| { + cx.emit(event.clone()); + })), + ) + .into_any_element() + } +} + +impl Render for ActivityRailView { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let top: Vec = self + .top_items + .clone() + .into_iter() + .map(|id| self.render_item(id, cx)) + .collect(); + let bottom: Vec = self + .bottom_items + .clone() + .into_iter() + .map(|id| self.render_item(id, cx)) + .collect(); + + v_flex() + .w(px(38.0)) + .h_full() + .flex_shrink_0() + .bg(ThemeColors::surface()) + .border_r_1() + .border_color(ThemeColors::border()) + .items_center() + .justify_between() + .py_2() + .child( + // 顶部视图组 + v_flex().w_full().items_center().gap_1().children(top), + ) + .child( + // 底部工具组 + v_flex().w_full().items_center().gap_1().children(bottom), + ) + } +} + +/// 右侧插件活动栏事件。 +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub enum PluginRailEvent { + OpenExtensions, + ToggleNotifications, + ToggleMaven, +} + +/// 右侧插件活动栏:Extensions / Notifications(带未读角标)/ Maven。 +#[allow(dead_code)] +pub struct PluginActivityRailView { + /// 通知未读数,用于角标;为 0 时不渲染角标。 + pub unread_notifications: usize, + /// Maven 是否可用(无 Maven 项目时隐藏入口)。 + pub maven_available: bool, +} + +impl EventEmitter for PluginActivityRailView {} + +#[allow(dead_code)] +impl Default for PluginActivityRailView { + fn default() -> Self { + Self::new() + } +} + +#[allow(dead_code)] +impl PluginActivityRailView { + pub fn new() -> Self { + Self { + unread_notifications: 0, + maven_available: true, + } + } + + /// 同步通知未读数(变化时才 notify,避免宿主每帧渲染循环)。 + pub fn set_unread(&mut self, count: usize, cx: &mut Context) { + if self.unread_notifications != count { + self.unread_notifications = count; + cx.notify(); + } + } + + /// 单个插件按钮:尺寸与左侧栏按钮一致;图标沿用按钮 Medium 尺寸(16px)。 + fn render_button( + &self, + id: &'static str, + icon: IconName, + tooltip: String, + event: PluginRailEvent, + cx: &mut Context, + ) -> AnyElement { + Button::new(id) + .ghost() + .icon(icon) + .tooltip(tooltip) + .w(px(32.0)) + .h(px(32.0)) + .rounded(px(4.0)) + .text_color(ThemeColors::subtle_foreground()) + .on_click(cx.listener(move |_this, _event, _window, cx| { + cx.emit(event.clone()); + })) + .into_any_element() + } +} + +impl Render for PluginActivityRailView { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let extensions = self.render_button( + "plugin-extensions", + IconName::Puzzle, + crate::i18n::menu_text(cx, "extensions.title").to_string(), + PluginRailEvent::OpenExtensions, + cx, + ); + let maven = self.maven_available.then(|| { + self.render_button( + "plugin-maven", + IconName::Box, + crate::i18n::menu_text(cx, "maven.title").to_string(), + PluginRailEvent::ToggleMaven, + cx, + ) + }); + + v_flex() + .w(px(38.0)) + .h_full() + .flex_shrink_0() + .bg(ThemeColors::surface()) + .border_l_1() + .border_color(ThemeColors::border()) + .items_center() + .pt_1() + .gap_1() + .child(extensions) + .child( + // 通知按钮 + 右上角未读角标。角标叠加在按钮容器上,避免撑开按钮尺寸。 + div() + .w(px(32.0)) + .h(px(32.0)) + .relative() + .flex_shrink_0() + .child(self.render_button( + "plugin-notifications", + IconName::Bell, + crate::i18n::menu_text(cx, "notifications.title").to_string(), + PluginRailEvent::ToggleNotifications, + cx, + )) + .when(self.unread_notifications > 0, |d| { + // 超过 9 条统一显示 9+,避免角标随数字变宽。 + let label = if self.unread_notifications > 9 { + "9+".to_string() + } else { + self.unread_notifications.to_string() + }; + d.child( + h_flex() + .absolute() + .top_0() + .right_0() + .min_w(px(14.0)) + .h(px(14.0)) + .rounded_full() + .bg(ThemeColors::destructive()) + .items_center() + .justify_center() + .text_xs() + .text_color(gpui_kit::white()) + .child(label), + ) + }), + ) + .when_some(maven, |rail, maven| rail.child(maven)) + } +} + +/// id → 图标,对齐 Tauri 侧边栏各项图标。 +fn item_icon(id: &str) -> IconName { + match id { + "files" => IconName::Files, + "git" => IconName::GitBranch, + "search" => IconName::Search, + "maven" => IconName::Box, + "run" => IconName::Play, + "terminal" => IconName::Terminal, + "diagnostics" => IconName::TriangleAlert, + "gitLog" => IconName::GitGraph, + _ => IconName::Settings, + } +} + +/// id → 本地化 tooltip(含快捷键后缀,文案对齐 Tauri;maven 无后缀)。 +fn item_tooltip(cx: &mut Context, id: &'static str) -> String { + let base: &str = match id { + "files" => crate::i18n::menu_text(cx, "workbench.project"), + "git" => crate::i18n::menu_text(cx, "workbench.changes"), + "search" => crate::i18n::menu_text(cx, "workbench.search"), + "run" => crate::i18n::menu_text(cx, "workbench.run"), + "terminal" => crate::i18n::menu_text(cx, "workbench.terminal"), + "diagnostics" => crate::i18n::menu_text(cx, "workbench.diagnostics"), + "gitLog" => crate::i18n::menu_text(cx, "workbench.gitLog"), + "settings" => crate::i18n::menu_text(cx, "workbench.settings"), + _ => crate::i18n::menu_text(cx, "maven.title"), + }; + let suffix = match id { + "files" => " (Ctrl+Shift+E)", + "git" => " (Ctrl+Shift+G)", + "search" => " (Ctrl+Shift+F)", + "terminal" => " (Ctrl+J)", + "diagnostics" => " (Ctrl+Shift+J)", + "run" => " (Shift+F10)", + "gitLog" => " (Alt+9)", + _ => "", + }; + format!("{base}{suffix}") +} + +/// 把持久化顺序规范化成合法且无重复的静态 id 列表,缺失项按默认顺序补齐, +/// 对齐 Tauri `normalizeItemOrder`。 +fn normalize_order(persisted: &[String]) -> Vec<&'static str> { + let mut out: Vec<&'static str> = Vec::new(); + for id in persisted { + if let Some(known) = static_id(id) { + if !out.contains(&known) { + out.push(known); + } + } + } + for id in SIDEBAR_ACTIVITY_ITEM_IDS.iter().copied() { + if !out.contains(&id) { + out.push(id); + } + } + out +} + +fn static_id(id: &str) -> Option<&'static str> { + SIDEBAR_ACTIVITY_ITEM_IDS + .iter() + .copied() + .find(|known| *known == id) +} + +/// 按后端能力过滤:search/git(含 gitLog)/terminal/diagnostics 由 `core_features` +/// 控制,其余(files/maven/run/settings)始终可见,对齐 Tauri +/// `sidebarActivityVisibilityItemIds`。 +fn is_feature_available(id: &str, features: &CoreFeatures) -> bool { + match id { + "search" => features.search, + "git" | "gitLog" => features.git, + "terminal" => features.terminal, + "diagnostics" => features.diagnostics, + _ => true, + } +} diff --git a/linux/src/workbench/bottom_panel.rs b/linux/src/workbench/bottom_panel.rs new file mode 100644 index 000000000..f8f015e78 --- /dev/null +++ b/linux/src/workbench/bottom_panel.rs @@ -0,0 +1,2299 @@ +use std::sync::mpsc; +use std::time::{Duration, Instant}; + +use gpui_kit::assets::IconName; +use gpui_kit::component::button::{Button, ButtonVariants as _}; +use gpui_kit::component::scroll::ScrollableElement as _; +use gpui_kit::component::{h_flex, v_flex, Disableable as _, Icon, Sizable as _}; +use gpui_kit::prelude::FluentBuilder as _; +use gpui_kit::EventEmitter; +use gpui_kit::{ + div, px, AnyElement, AppContext as _, Context, Entity, FontWeight, InteractiveElement as _, + IntoElement, ParentElement as _, Render, ScrollHandle, StatefulInteractiveElement as _, + Styled as _, WeakEntity, Window, +}; + +use crate::core::CoreClient; +use crate::lsp; +use crate::theme::ThemeColors; +use crate::workbench::editor::EditorView; +use crate::workbench::run::{ + create_launch_plan_request, default_generated_configuration_id, list_java_sources, + maven_context_for_configuration, parse_resolved_configurations, read_toolchain_paths, + sequence_is_current, toolchain_candidates, write_generated_documents, LaunchPlan, OutputStream, + ProcessEvent, ProcessManager, RunConfigItem, +}; +use crate::workbench::terminal::TerminalView; +use crate::workbench::view::WorkbenchView; + +/// 运行历史上限。 +const MAX_RUN_HISTORY: usize = 50; + +/// Run 输出保留上限(行),超出丢弃最旧。 +const MAX_RUN_OUTPUT: usize = 2000; + +/// Run 面板工程状态,对齐 Tauri RunPane 的 missing/ready/invalid 三态 +/// (Linux 可用子集:invalid 统一为 `Failed` 并携带 core 错误文案)。 +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RunProjectState { + /// 无可运行配置(非 Java/Maven/npm 工程或配置为空)。 + Missing, + /// 正在经 core 探测工程与配置。 + Loading, + /// 配置列表可用。 + Ready, + /// 探测失败,内附展示文案。 + Failed(String), +} + +/// 上次 Maven 目标参数(头部重跑按钮回放,对齐 Tauri `rerunLastTest` 语义的子集)。 +#[derive(Debug, Clone)] +struct MavenGoalParams { + pom_path: String, + goal: String, + target: String, + profiles: Vec, + skip_tests: bool, +} + +/// Git 提交记录行数上限。 +const MAX_GIT_LOG_ENTRIES: usize = 50; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BottomTab { + Terminal, + Run, + Maven, + Diagnostics, + GitLog, +} + +/// 诊断面板中的一条问题,字段与 Tauri DiagnosticsBuffer 的展示模型对齐。 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DiagnosticEntry { + pub severity: String, + pub file_path: String, + pub line: u32, + pub column: u32, + pub message: String, + pub source: Option, + pub code: Option, +} + +/// 诊断面板向外发出的定位事件。 +#[derive(Debug, Clone)] +pub enum BottomPanelEvent { + OpenFile { + path: String, + line: u32, + }, + /// 面板清空按钮:请求宿主同步丢弃缓存的 LSP 诊断。 + ClearDiagnostics, +} + +/// Git 提交记录的一行:短 hash + 首行 message,只读展示不跳转。 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GitLogEntry { + pub hash: String, + pub message: String, +} + +pub struct BottomPanelView { + pub active_tab: BottomTab, + pub is_collapsed: bool, + pub height: f32, + pub terminal: Entity, + pub diagnostics: Vec, + /// 终端工作目录;GitLog 面板取数时作为 `git -C` 目标。 + pub working_dir: String, + /// 运行历史(`TerminalView::send_command` 被调用时由宿主经 + /// [`BottomPanelView::record_run`] 落入),上限 [`MAX_RUN_HISTORY`]。 + pub run_history: Vec, + /// Run 面板状态机(Tauri RunPane 的 Linux 可用子集)。 + pub(crate) run_state: RunProjectState, + /// Run 面板工程展示名(工作区目录名)。 + pub(crate) run_project_name: String, + /// Run 配置列表(Core resolve 的有效配置)。 + pub(crate) run_configs: Vec, + /// Core resolve 给出的默认配置 id。 + pub(crate) default_run_config: Option, + /// 选中的 Run 配置 id。 + pub(crate) selected_run_config: Option, + /// reload 尚未完成时,行内 Run 要在 ready 后启动的指定 id。 + pending_run_id: Option, + /// reload 完成后自动启动 Core 选出的配置。 + run_on_ready: bool, + /// Run/配置加载诊断,按 Core 返回顺序展示。 + pub(crate) run_diagnostics: Vec, + /// 运行输出行(后台线程经 channel 推送,`cx.spawn` 泵入)。 + pub(crate) run_output: Vec, + /// 是否有 Run execution 在准备或运行。 + pub(crate) run_running: bool, + /// 最近一次 Run 主进程退出码。 + pub(crate) run_exit_code: Option, + /// 输出跟随末尾(对齐 Tauri `scrollOutputToEnd`,默认开,落盘持久化)。 + pub(crate) run_follow_end: bool, + /// 运行输出滚动句柄(跟随末尾用)。 + run_scroll: ScrollHandle, + /// 上次跟随到的输出行数(只在新增时滚动,避免无关重绘抢夺滚动条)。 + run_followed_len: usize, + /// Maven 任务标题(对齐 Tauri `taskTitle`,如 `compile · pom.xml`)。 + pub(crate) maven_title: Option, + /// Maven 任务输出行(`$ mvn …` 开头,对齐 Tauri Maven 页)。 + pub(crate) maven_output: Vec, + /// Maven 任务是否在跑。 + pub(crate) maven_running: bool, + /// core 客户端(`reload_run_project` / `createLaunchPlan` 经它走 core JSON 命令)。 + client: CoreClient, + /// 工作台持有的 Java LSP owner;使用弱引用避免父子 Entity 循环。 + workbench: Option>, + /// 所有 Run/Maven 子进程的 session/execution 所有者。 + processes: ProcessManager, + /// Maven 启动序号,丢弃过期 `launchPlan` 结果。 + maven_seq: u64, + /// 上次 Maven 目标参数(头部重跑按钮回放)。 + last_maven_goal: Option, + /// reload 序号,丢弃过期探测结果。 + run_seq: u64, + /// Run execution 序号,阻止被替换的 session 继续写输出。 + run_execution_seq: u64, + /// 当前编辑器,Run 前用于保存脏文件。 + run_editor: Option>, + /// 最近一次加载的 Git 提交记录。 + pub git_log: Vec, + /// Git 记录加载失败时的展示文案;成功后清空。 + pub git_log_error: Option, +} + +impl EventEmitter for BottomPanelView {} + +/// 等待 Workbench 轮询到指定 Core LSP 操作结果,并拒绝 workspace/session 替换。 +async fn wait_for_lsp_operation( + workbench: &Entity, + session_id: &str, + operation_id: &str, + timeout: Duration, + run_guard: Option<(&Entity, u64, u64)>, + cx: &mut gpui_kit::AsyncApp, +) -> Result { + let deadline = Instant::now() + timeout; + loop { + if let Some((owner, reload_seq, execution_seq)) = run_guard { + if !owner.read_with(cx, |view, _app| { + view.is_current_run_execution(reload_seq, execution_seq) + }) { + return Err( + "Run was replaced or cancelled before Java preparation completed.".to_string(), + ); + } + } + let status = workbench.read_with(cx, |workbench, _app| workbench.java_lsp_status()); + if status.session_id.as_deref() != Some(session_id) { + return Err( + "Java language-server session was replaced before the request completed." + .to_string(), + ); + } + if status.state == "failed" || status.state == "stopped" { + return Err(status.error.unwrap_or_else(|| { + "Java language-server session stopped before the request completed.".to_string() + })); + } + if let Some(result) = workbench.update(cx, |workbench, _cx| { + workbench.take_lsp_operation_result(session_id, operation_id) + }) { + return result; + } + if Instant::now() >= deadline { + return Err(format!( + "Java language-server request {operation_id} timed out after {} seconds.", + timeout.as_secs() + )); + } + cx.background_executor() + .timer(Duration::from_millis(100)) + .await; + } +} + +async fn wait_for_java_entrypoints( + workbench: &Entity, + client: &CoreClient, + timeout: Duration, + cx: &mut gpui_kit::AsyncApp, +) -> Result, String> { + workbench.update(cx, |workbench, cx| workbench.ensure_java_lsp(cx)); + let deadline = Instant::now() + timeout; + loop { + let status = workbench.read_with(cx, |workbench, _app| workbench.java_lsp_status()); + if status.state == "failed" || status.state == "stopped" { + return Ok(None); + } + if lsp::java_lsp_allows_run(&status) { + let Some(session_id) = status.session_id else { + return Err("Java language service became ready without a session id.".to_string()); + }; + let operation = client + .execute::( + cx, + "lsp.request", + lsp::java_entrypoints_request(&session_id), + ) + .await?; + let operation_id = operation + .get("operationId") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| { + "Java entrypoint request did not return an operationId.".to_string() + })?; + let result = wait_for_lsp_operation( + workbench, + &session_id, + operation_id, + Duration::from_secs(35), + None, + cx, + ) + .await?; + return lsp::parse_java_entrypoints(&result).map(Some); + } + if Instant::now() >= deadline { + return Err( + "Java language service did not reach a runnable project state before the configuration deadline." + .to_string(), + ); + } + cx.background_executor() + .timer(Duration::from_millis(200)) + .await; + } +} + +async fn prepare_java_run_launch( + workbench: &Entity, + client: &CoreClient, + item: &RunConfigItem, + root: &str, + run_guard: (&Entity, u64, u64), + cx: &mut gpui_kit::AsyncApp, +) -> Result<(serde_json::Value, String), String> { + let source_path = item + .source + .as_deref() + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| "Java run configuration is missing its source path.".to_string())?; + let configured_main = item.main_class.as_deref(); + let source_key = std::path::Path::new(source_path) + .strip_prefix(root) + .map(|path| path.to_string_lossy().replace('\\', "/")) + .unwrap_or_else(|_| source_path.to_string()); + let source_for_build = lsp::absolute_path(root, source_path); + workbench.update(cx, |workbench, cx| workbench.ensure_java_lsp(cx)); + let ready_deadline = Instant::now() + Duration::from_secs(10 * 60); + let session_id = loop { + if !run_guard.0.read_with(cx, |view, _app| { + view.is_current_run_execution(run_guard.1, run_guard.2) + }) { + return Err( + "Run was replaced or cancelled before Java preparation completed.".to_string(), + ); + } + let status = workbench.read_with(cx, |workbench, _app| workbench.java_lsp_status()); + if status.state == "failed" || status.state == "stopped" { + return Err(status + .error + .unwrap_or_else(|| "Java language service is not ready.".to_string())); + } + if lsp::java_lsp_allows_run(&status) { + if let Some(session_id) = status.session_id { + break session_id; + } + } + if Instant::now() >= ready_deadline { + return Err( + "Java language service did not reach ServiceReady before the run deadline." + .to_string(), + ); + } + cx.background_executor() + .timer(Duration::from_millis(200)) + .await; + }; + + let entrypoints_operation = client + .execute::( + cx, + "lsp.request", + lsp::java_entrypoints_request(&session_id), + ) + .await?; + let entrypoints_operation_id = entrypoints_operation + .get("operationId") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| "Java entrypoint request did not return an operationId.".to_string())?; + let entrypoints = lsp::parse_java_entrypoints( + &wait_for_lsp_operation( + workbench, + &session_id, + entrypoints_operation_id, + Duration::from_secs(35), + Some(run_guard), + cx, + ) + .await?, + )?; + let entrypoint = lsp::select_java_entrypoint(&entrypoints, &source_key, configured_main)?; + let main_class = entrypoint + .get("mainClass") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| "Java entrypoint result is missing mainClass.".to_string())?; + let project_name = entrypoint + .get("projectName") + .and_then(serde_json::Value::as_str) + .map(str::to_string); + + let build_operation = client + .execute::( + cx, + "lsp.request", + lsp::java_build_request( + &session_id, + &source_for_build, + main_class, + project_name.as_deref(), + ), + ) + .await?; + let build_operation_id = build_operation + .get("operationId") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| "Java build request did not return an operationId.".to_string())?; + let build_result = wait_for_lsp_operation( + workbench, + &session_id, + build_operation_id, + Duration::from_secs(10 * 60), + Some(run_guard), + cx, + ) + .await; + let build_error = match &build_result { + Ok(value) => lsp::parse_java_build_result(value).err(), + Err(error) => Some(error.clone()), + }; + + let classpath_operation = client + .execute::( + cx, + "lsp.request", + lsp::java_classpath_request(&session_id, main_class, project_name.as_deref()), + ) + .await?; + let classpath_operation_id = classpath_operation + .get("operationId") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| "Java classpath request did not return an operationId.".to_string())?; + let classpath = match wait_for_lsp_operation( + workbench, + &session_id, + classpath_operation_id, + Duration::from_secs(35), + Some(run_guard), + cx, + ) + .await + { + Ok(value) => value, + Err(error) => { + if let Some(build_error) = &build_error { + return Err(format!( + "Java project build failed: {build_error}; runtime classpath resolution failed: {error}" + )); + } + return Err(error); + } + }; + let launch = match lsp::java_launch_payload(&entrypoint, &classpath) { + Ok(launch) => launch, + Err(error) => { + if let Some(build_error) = &build_error { + return Err(format!( + "Java project build failed: {build_error}; runtime classpath result was invalid: {error}" + )); + } + return Err(error); + } + }; + if let Some(error) = build_error { + return Err(format!("Java project build failed: {error}")); + } + Ok((launch, session_id)) +} + +impl BottomPanelView { + pub fn new(working_dir: String, cx: &mut Context) -> Self { + let terminal = cx.new(|cx| TerminalView::new(working_dir.clone(), cx)); + + Self { + active_tab: BottomTab::Terminal, + is_collapsed: true, + height: 240.0, + terminal, + diagnostics: Vec::new(), + working_dir: working_dir.clone(), + run_history: Vec::new(), + run_state: RunProjectState::Missing, + run_project_name: crate::settings::project_dir_name(&working_dir).to_string(), + run_configs: Vec::new(), + default_run_config: None, + selected_run_config: None, + pending_run_id: None, + run_on_ready: false, + run_diagnostics: Vec::new(), + run_output: Vec::new(), + run_running: false, + run_exit_code: None, + run_follow_end: crate::settings::get(cx).run_scroll_to_end, + run_scroll: ScrollHandle::new(), + run_followed_len: 0, + maven_title: None, + maven_output: Vec::new(), + maven_running: false, + client: CoreClient::new(), + workbench: None, + processes: ProcessManager::new(), + maven_seq: 0, + last_maven_goal: None, + run_seq: 0, + run_execution_seq: 0, + run_editor: None, + git_log: Vec::new(), + git_log_error: None, + } + } + + pub fn is_visible(&self) -> bool { + !self.is_collapsed + } + + fn render_diagnostics_panel(&self, cx: &mut Context) -> AnyElement { + if self.diagnostics.is_empty() { + return div() + .size_full() + .flex() + .items_center() + .justify_center() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(crate::i18n::menu_text(cx, "diagnostics.empty").to_string()) + .into_any_element(); + } + + let rows: Vec = self + .diagnostics + .iter() + .enumerate() + .map(|(index, diagnostic)| { + let location = format!( + "{}:{}:{}", + diagnostic.file_path, + diagnostic.line + 1, + diagnostic.column + 1 + ); + let source = diagnostic + .source + .as_deref() + .unwrap_or("diagnostic") + .to_string(); + let path = diagnostic.file_path.clone(); + let line = diagnostic.line + 1; + let severity_color = match diagnostic.severity.as_str() { + "error" => ThemeColors::destructive(), + "warning" => ThemeColors::warning(), + _ => ThemeColors::accent_blue(), + }; + h_flex() + .id(format!("diagnostic-{index}")) + .w_full() + .items_start() + .gap_2() + .px_3() + .py_2() + .cursor_pointer() + .hover(|row| row.bg(ThemeColors::accent())) + .child( + div() + .text_xs() + .font_weight(FontWeight::BOLD) + .text_color(severity_color) + .child(diagnostic.severity.clone()), + ) + .child( + v_flex() + .flex_1() + .min_w_0() + .gap_1() + .child( + div() + .text_xs() + .text_color(ThemeColors::text_primary()) + .child(diagnostic.message.clone()), + ) + .child( + div() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(format!("{location} · {source}")), + ), + ) + .on_click(cx.listener(move |_this, _event, _window, cx| { + cx.emit(BottomPanelEvent::OpenFile { + path: path.clone(), + line, + }); + })) + .into_any_element() + }) + .collect(); + + div() + .size_full() + .overflow_y_scrollbar() + .children(rows) + .into_any_element() + } + + pub fn set_height(&mut self, height: f32, cx: &mut Context) { + self.height = height; + cx.notify(); + } + + pub fn set_tab(&mut self, tab: BottomTab, cx: &mut Context) { + self.active_tab = tab; + self.is_collapsed = false; + if tab == BottomTab::GitLog { + // 打开即刷新提交记录;refresh_git_log 内已 notify。 + self.refresh_git_log(cx); + return; + } + if tab == BottomTab::Run { + // 打开即重探 Run 工程;reload_run_project 内已 notify。 + self.reload_run_project(cx); + return; + } + cx.notify(); + } + + /// 用最新一轮 LSP 诊断替换面板内容;顺序与 [`crate::lsp`] 投影一致。 + pub fn set_diagnostics(&mut self, diagnostics: Vec, cx: &mut Context) { + self.diagnostics = diagnostics; + cx.notify(); + } + + pub fn toggle_collapsed(&mut self, cx: &mut Context) { + self.is_collapsed = !self.is_collapsed; + cx.notify(); + } + + /// 记录一次运行命令;空命令忽略,超出上限丢弃最旧记录。 + /// 宿主在转调 `TerminalView::send_command` 时调用。 + pub fn record_run(&mut self, cmd: &str, cx: &mut Context) { + let cmd = cmd.trim(); + if cmd.is_empty() { + return; + } + self.run_history.push(cmd.to_string()); + if self.run_history.len() > MAX_RUN_HISTORY { + let overflow = self.run_history.len() - MAX_RUN_HISTORY; + self.run_history.drain(..overflow); + } + cx.notify(); + } + + /// 更新工作目录并重探 Run 工程(替代直接写 `working_dir` 字段)。 + pub fn set_working_dir(&mut self, dir: String, cx: &mut Context) { + let _ = self.processes.request_stop("run", None); + self.maven_seq += 1; + let _ = self.processes.request_stop("maven", None); + self.maven_running = false; + self.pending_run_id = None; + self.run_on_ready = false; + self.working_dir = dir; + self.reload_run_project(cx); + } + + /// 设置 Run 前需要保存的编辑器实体。 + pub fn set_run_editor(&mut self, editor: Option>, cx: &mut Context) { + self.run_editor = editor; + cx.notify(); + } + + /// 设置工作台 Java LSP owner。Run 不直接创建第二个 JDT session。 + pub fn set_workbench(&mut self, workbench: Entity, cx: &mut Context) { + self.workbench = Some(workbench.downgrade()); + cx.notify(); + } + + /// 重探 Run 工程:先 inspect,再按需 generate,随后持久化并 resolve。 + pub fn reload_run_project(&mut self, cx: &mut Context) { + self.run_seq += 1; + self.run_execution_seq += 1; + let seq = self.run_seq; + if self.run_running { + let _ = self.processes.request_stop("run", None); + self.run_running = false; + } + self.run_project_name = crate::settings::project_dir_name(&self.working_dir).to_string(); + self.run_configs.clear(); + self.default_run_config = None; + self.selected_run_config = None; + self.run_diagnostics.clear(); + self.run_state = RunProjectState::Loading; + cx.notify(); + + let client = self.client.clone(); + let root = self.working_dir.clone(); + let workbench = self.workbench.as_ref().and_then(WeakEntity::upgrade); + cx.spawn(async move |this, cx| { + let inspection = client + .execute::( + &cx, + "runConfig.inspect", + serde_json::json!({ "root": root, "checkFingerprint": true }), + ) + .await; + let inspection = match inspection { + Ok(value) => value, + Err(error) => { + let _ = this.update(cx, |view, cx| { + if view.run_seq == seq { + view.run_state = RunProjectState::Failed(error); + cx.notify(); + } + }); + return; + } + }; + let status = inspection + .get("status") + .and_then(serde_json::Value::as_str) + .unwrap_or("missing"); + let inspection_diagnostics = inspection + .get("diagnostics") + .and_then(serde_json::Value::as_array) + .map(|items| { + items + .iter() + .filter_map(|item| { + item.get("message") + .and_then(serde_json::Value::as_str) + .map(str::to_string) + }) + .collect::>() + }) + .unwrap_or_default(); + let inspection_diagnostics = if status == "ready" { + inspection_diagnostics + } else { + Vec::new() + }; + if status != "ready" { + let paths = list_java_sources(&root); + let java_entrypoints = if paths.is_empty() { + None + } else if let Some(workbench) = workbench.as_ref() { + match wait_for_java_entrypoints( + workbench, + &client, + Duration::from_secs(10 * 60), + cx, + ) + .await + { + Ok(value) => value, + Err(error) => { + let _ = this.update(cx, |view, cx| { + if view.run_seq == seq { + view.run_state = RunProjectState::Failed(error); + cx.notify(); + } + }); + return; + } + } + } else { + None + }; + let mut generate_payload = serde_json::json!({ + "root": root, + "paths": paths, + "modulePaths": [] + }); + if let Some(entrypoints) = java_entrypoints { + generate_payload["javaEntrypoints"] = entrypoints; + } + let generated = client + .execute::( + &cx, + "runConfig.generate", + generate_payload, + ) + .await; + let generated = match generated { + Ok(value) => value, + Err(error) => { + let _ = this.update(cx, |view, cx| { + if view.run_seq == seq { + view.run_state = RunProjectState::Failed(error); + cx.notify(); + } + }); + return; + } + }; + if !this + .update(cx, |view, _cx| view.run_seq == seq) + .unwrap_or(false) + { + return; + } + let generated_document = generated.get("generated").cloned().unwrap_or_default(); + let requirements = generated + .get("toolchainRequirements") + .cloned() + .unwrap_or_else(|| serde_json::json!({"version": 1, "toolchains": {}})); + let default_id = default_generated_configuration_id(&generated_document); + if let Err(error) = write_generated_documents( + &root, + &generated_document, + &requirements, + default_id.as_deref(), + ) { + let _ = this.update(cx, |view, cx| { + if view.run_seq == seq { + view.run_state = RunProjectState::Failed(error); + cx.notify(); + } + }); + return; + } + if !this + .update(cx, |view, _cx| view.run_seq == seq) + .unwrap_or(false) + { + return; + } + } + + let toolchains = read_toolchain_paths(&root); + let candidates = toolchain_candidates(&root, &toolchains); + let resolved = client + .execute::( + &cx, + "runConfig.resolve", + serde_json::json!({ + "root": root, + "toolchainCandidates": candidates + }), + ) + .await; + let resolved = match resolved { + Ok(value) => value, + Err(error) => { + let _ = this.update(cx, |view, cx| { + if view.run_seq == seq { + view.run_state = RunProjectState::Failed(error); + cx.notify(); + } + }); + return; + } + }; + let parsed = match parse_resolved_configurations(&resolved) { + Ok(value) => value, + Err(error) => { + let _ = this.update(cx, |view, cx| { + if view.run_seq == seq { + view.run_state = RunProjectState::Failed(error); + cx.notify(); + } + }); + return; + } + }; + let launch = this.update(cx, |view, cx| { + if view.run_seq != seq { + return None; + } + view.run_configs = parsed.configurations; + view.default_run_config = parsed.default_configuration_id.clone(); + view.run_diagnostics = inspection_diagnostics.clone(); + view.run_diagnostics.extend(parsed.diagnostics.clone()); + view.run_diagnostics.dedup(); + let selected = view + .pending_run_id + .clone() + .filter(|id| view.run_configs.iter().any(|item| &item.id == id)) + .or_else(|| parsed.default_configuration_id.clone()) + .or_else(|| view.run_configs.first().map(|item| item.id.clone())); + view.selected_run_config = selected.clone(); + view.run_state = if view.run_configs.is_empty() { + RunProjectState::Missing + } else { + RunProjectState::Ready + }; + let should_launch = view.run_on_ready || view.pending_run_id.is_some(); + view.run_on_ready = false; + cx.notify(); + let launch_id = view + .pending_run_id + .take() + .or_else(|| should_launch.then(|| selected.clone()).flatten()); + launch_id.and_then(|id| view.run_configs.iter().find(|item| item.id == id).cloned()) + }); + if let Ok(Some(item)) = launch { + let _ = this.update(cx, |view, cx| view.start_run(item, cx)); + } + }) + .detach(); + } + + /// 顶部 Run 只在当前 reload 结果 ready 后使用默认/当前选择。 + pub fn run_selected_config(&mut self, cx: &mut Context) { + if self.run_running { + self.stop_running(cx); + return; + } + if !matches!(self.run_state, RunProjectState::Ready) { + self.run_on_ready = true; + if !matches!(self.run_state, RunProjectState::Loading) { + self.reload_run_project(cx); + } else { + cx.notify(); + } + return; + } + if let Some(item) = self.selected_run_item() { + self.start_run(item, cx); + } else { + self.run_on_ready = true; + self.reload_run_project(cx); + } + } + + /// 行内 Run 永远携带用户指定的配置 id;配置尚未 ready 时等待 reload。 + pub fn run_configuration_id(&mut self, id: String, cx: &mut Context) { + if self.run_running { + return; + } + self.pending_run_id = Some(id.clone()); + self.selected_run_config = Some(id.clone()); + self.run_on_ready = true; + if matches!(self.run_state, RunProjectState::Ready) + && self.run_configs.iter().any(|item| item.id == id) + { + self.pending_run_id = None; + self.run_on_ready = false; + if let Some(item) = self.run_configs.iter().find(|item| item.id == id).cloned() { + self.start_run(item, cx); + } + } else if !matches!(self.run_state, RunProjectState::Loading) { + self.reload_run_project(cx); + } else { + cx.notify(); + } + } + + fn start_run(&mut self, item: RunConfigItem, cx: &mut Context) { + if self.run_running { + return; + } + self.run_execution_seq += 1; + let execution_seq = self.run_execution_seq; + self.run_running = true; + self.run_exit_code = None; + push_run_line( + &mut self.run_output, + format!( + "{}:{}", + crate::i18n::menu_text(cx, "run.starting"), + item.name + ), + ); + if Self::needs_java_project_preparation(&item) { + push_run_line( + &mut self.run_output, + run_ui_text( + cx, + "等待 Java 语言服务完成项目准备…", + "Waiting for Java language service preparation…", + ), + ); + } + cx.notify(); + let launch_seq = self.run_seq; + let launch_execution_seq = execution_seq; + let client = self.client.clone(); + let root = self.working_dir.clone(); + let editor = self.run_editor.clone(); + let workbench = self.workbench.as_ref().and_then(WeakEntity::upgrade); + let processes = self.processes.clone(); + cx.spawn(async move |this, cx| { + let run_owner = this.upgrade().expect("Run owner must remain alive while preparing"); + if let Some(editor) = editor.as_ref() { + let save = editor.update(cx, |editor, cx| editor.save_active_task(cx)); + if let Err(error) = save.await { + let _ = this.update(cx, |view, cx| { + if view.is_current_run_execution(launch_seq, launch_execution_seq) { + view.run_running = false; + push_run_line(&mut view.run_output, error); + cx.notify(); + } + }); + return; + } + } + if this + .update(cx, |view, _cx| { + !view.is_current_run_execution(launch_seq, launch_execution_seq) + }) + .unwrap_or(true) + { + return; + } + let current_file = editor + .as_ref() + .and_then(|editor| editor.update(cx, |editor, _cx| editor.active_file_path())); + let current_file = current_file.and_then(|path| workspace_relative_path(&root, &path)); + let mut toolchains = read_toolchain_paths(&root); + if let Some(path) = item.java_home_path() { + toolchains.java_home_path = path.to_string(); + } + if let Some(path) = item.maven_executable_path() { + toolchains.maven_executable_path = path.to_string(); + } + if let Some(path) = item.maven_java_home_path() { + toolchains.maven_java_home_path = path.to_string(); + } + let maven_context = item + .uses_maven() + .then(|| maven_context_for_configuration(&item, &toolchains)); + let java_launch = if Self::needs_java_project_preparation(&item) { + match workbench.as_ref() { + Some(workbench) => { + prepare_java_run_launch( + workbench, + &client, + &item, + &root, + (&run_owner, launch_seq, launch_execution_seq), + cx, + ) + .await + .map(Some) + } + None => Err( + "Java language service is unavailable because the workbench owner is missing." + .to_string(), + ), + } + } else { + Ok(None) + }; + let java_launch = match java_launch { + Ok(value) => value, + Err(error) => { + let _ = this.update(cx, |view, cx| { + if view.is_current_run_execution(launch_seq, launch_execution_seq) { + view.run_running = false; + view.run_exit_code = Some(1); + view.run_diagnostics.push(error.clone()); + view.run_diagnostics.dedup(); + push_run_line(&mut view.run_output, error); + cx.notify(); + } + }); + return; + } + }; + if let (Some((_, session_id)), Some(workbench)) = + (java_launch.as_ref(), workbench.as_ref()) + { + let current_session = workbench + .read_with(cx, |workbench, _app| workbench.java_lsp_status()) + .session_id; + if current_session.as_deref() != Some(session_id.as_str()) { + let error = "Java language-server session changed before launch planning."; + let _ = this.update(cx, |view, cx| { + if view.is_current_run_execution(launch_seq, launch_execution_seq) { + view.run_running = false; + view.run_exit_code = Some(1); + view.run_diagnostics.push(error.to_string()); + push_run_line(&mut view.run_output, error.to_string()); + cx.notify(); + } + }); + return; + } + } + let payload = create_launch_plan_request( + &root, + &item, + current_file.as_deref(), + maven_context.as_ref(), + java_launch.as_ref().map(|(value, _)| value), + ); + let plan = client + .execute::( + &cx, + "runConfig.createLaunchPlan", + payload, + ) + .await; + if this + .update(cx, |view, _cx| { + !view.is_current_run_execution(launch_seq, launch_execution_seq) + }) + .unwrap_or(true) + { + return; + } + let plan = match plan { + Ok(value) => value, + Err(error) => { + let _ = this.update(cx, |view, cx| { + if view.is_current_run_execution(launch_seq, launch_execution_seq) { + view.run_running = false; + push_run_line(&mut view.run_output, error); + cx.notify(); + } + }); + return; + } + }; + let plan = match LaunchPlan::from_value(&plan) { + Ok(plan) => plan, + Err(error) => { + let _ = this.update(cx, |view, cx| { + if view.is_current_run_execution(launch_seq, launch_execution_seq) { + view.run_running = false; + push_run_line(&mut view.run_output, error); + cx.notify(); + } + }); + return; + } + }; + let (steps, _main) = + match crate::workbench::run::process::resolve_launch(&plan, &root, &toolchains) { + Ok(value) => value, + Err(error) => { + let _ = this.update(cx, |view, cx| { + if view.is_current_run_execution(launch_seq, launch_execution_seq) { + view.run_running = false; + push_run_line(&mut view.run_output, error); + cx.notify(); + } + }); + return; + } + }; + if this + .update(cx, |view, _cx| { + !view.is_current_run_execution(launch_seq, launch_execution_seq) + }) + .unwrap_or(true) + { + return; + } + let execution_id = uuid::Uuid::new_v4().to_string(); + let (sender, receiver) = mpsc::channel::(); + let handle = match processes.start("run", &execution_id, steps, sender) { + Ok(handle) => handle, + Err(error) => { + let _ = this.update(cx, |view, cx| { + if view.is_current_run_execution(launch_seq, launch_execution_seq) { + view.run_running = false; + push_run_line(&mut view.run_output, error); + cx.notify(); + } + }); + return; + } + }; + let receiver = std::sync::Arc::new(std::sync::Mutex::new(receiver)); + loop { + let slot = receiver.clone(); + let event = cx + .background_executor() + .spawn(async move { + let guard = slot.lock().ok()?; + Some( + match guard.recv_timeout(std::time::Duration::from_secs(1)) { + Ok(event) => Ok(event), + Err(error) => Err(error), + }, + ) + }) + .await; + let event = match event { + Some(Ok(event)) => event, + Some(Err(mpsc::RecvTimeoutError::Timeout)) => continue, + Some(Err(mpsc::RecvTimeoutError::Disconnected)) | None => break, + }; + let finished = matches!(&event, ProcessEvent::Finished { .. }); + let stale = this + .update(cx, |view, _cx| { + !view.is_current_run_execution(launch_seq, launch_execution_seq) + }) + .unwrap_or(true); + if stale { + let _ = processes.stop("run", Some(&execution_id)); + break; + } + let _ = this.update(cx, |view, cx| match event { + ProcessEvent::Started { + index: _index, + label, + } => { + push_run_line(&mut view.run_output, label.clone()); + view.record_run(&label, cx); + cx.notify(); + } + ProcessEvent::Output { stream, text } => { + let text = if stream == OutputStream::Stderr { + format!("[stderr] {text}") + } else { + text + }; + push_run_line(&mut view.run_output, text); + cx.notify(); + } + ProcessEvent::Finished { + exit_code, + cancelled, + error, + } => { + if let Some(error) = error { + push_run_line(&mut view.run_output, error); + } + let exit_code = exit_code.or_else(|| Some(1)); + if cancelled { + push_run_line( + &mut view.run_output, + crate::i18n::menu_text(cx, "run.finished").to_string(), + ); + } else { + push_run_line( + &mut view.run_output, + format!( + "{}:{}", + crate::i18n::menu_text(cx, "run.exited"), + exit_code.unwrap_or(1) + ), + ); + } + view.run_exit_code = exit_code; + view.run_running = false; + cx.notify(); + } + }); + if finished { + break; + } + } + let _ = handle.join(); + }) + .detach(); + } + + /// 停止在跑进程;ProcessManager 异步终止完整进程树并在输出泵中回收。 + pub fn stop_running(&mut self, cx: &mut Context) { + let mut stopped = false; + if self.run_running { + self.run_execution_seq += 1; + let _ = self.processes.request_stop("run", None); + push_run_line( + &mut self.run_output, + crate::i18n::menu_text(cx, "run.stopping").to_string(), + ); + self.run_running = false; + stopped = true; + } + if self.maven_running { + self.maven_seq += 1; + let _ = self.processes.request_stop("maven", None); + push_run_line( + &mut self.maven_output, + crate::i18n::menu_text(cx, "run.stopping").to_string(), + ); + self.maven_running = false; + stopped = true; + } + if stopped { + cx.notify(); + } + } + /// 是否发生过 Maven 运行;宿主据此决定左侧栏是否展示 maven 项 + ///(对齐 Tauri `hasMavenRun`:任务跑过即真,与终端历史无关)。 + pub fn has_maven_run(&self) -> bool { + self.maven_running || !self.maven_output.is_empty() + } + + /// 运行 Maven 目标:Core 生成计划,Linux 只解析工具链并托管进程。 + pub fn run_maven_goal( + &mut self, + pom_path: &str, + goal: &str, + target: &str, + profiles: &[String], + skip_tests: bool, + cx: &mut Context, + ) { + let goal = goal.trim().to_string(); + if goal.is_empty() { + return; + } + if self.maven_running { + let _ = self.processes.request_stop("maven", None); + } + self.maven_seq += 1; + let seq = self.maven_seq; + let title = format!("{goal} · {target}"); + self.last_maven_goal = Some(MavenGoalParams { + pom_path: pom_path.to_string(), + goal: goal.clone(), + target: target.to_string(), + profiles: profiles.to_vec(), + skip_tests, + }); + self.active_tab = BottomTab::Maven; + self.is_collapsed = false; + self.maven_title = Some(title); + self.maven_output.clear(); + self.maven_running = true; + cx.notify(); + + let client = self.client.clone(); + let root = self.working_dir.clone(); + let module = maven_module_for_pom(&self.working_dir, pom_path); + let profiles = profiles.to_vec(); + let processes = self.processes.clone(); + cx.spawn(async move |this, cx| { + let plan = client + .execute::( + &cx, + "maven.launchPlan", + serde_json::json!({ + "root": root, + "context": { + "version": 1, + "reactorPath": ".", + "profiles": profiles, + "skipTests": skip_tests, + }, + "module": module, + "goals": [goal], + }), + ) + .await; + if this + .update(cx, |view, _cx| view.maven_seq != seq) + .unwrap_or(true) + { + return; + } + let plan = match plan { + Ok(value) => value, + Err(error) => { + let _ = this.update(cx, |view, cx| { + if view.maven_seq == seq { + view.maven_running = false; + push_run_line(&mut view.maven_output, error); + cx.notify(); + } + }); + return; + } + }; + let plan = match LaunchPlan::from_value(&plan) { + Ok(plan) => plan, + Err(error) => { + let _ = this.update(cx, |view, cx| { + if view.maven_seq == seq { + view.maven_running = false; + push_run_line(&mut view.maven_output, error); + cx.notify(); + } + }); + return; + } + }; + let toolchains = read_toolchain_paths(&root); + let (steps, _main) = + match crate::workbench::run::process::resolve_launch(&plan, &root, &toolchains) { + Ok(value) => value, + Err(error) => { + let _ = this.update(cx, |view, cx| { + if view.maven_seq == seq { + view.maven_running = false; + push_run_line(&mut view.maven_output, error); + cx.notify(); + } + }); + return; + } + }; + if this + .update(cx, |view, _cx| view.maven_seq != seq) + .unwrap_or(true) + { + return; + } + let execution_id = uuid::Uuid::new_v4().to_string(); + let (sender, receiver) = mpsc::channel::(); + let handle = match processes.start("maven", &execution_id, steps, sender) { + Ok(handle) => handle, + Err(error) => { + let _ = this.update(cx, |view, cx| { + if view.maven_seq == seq { + view.maven_running = false; + push_run_line(&mut view.maven_output, error); + cx.notify(); + } + }); + return; + } + }; + let receiver = std::sync::Arc::new(std::sync::Mutex::new(receiver)); + loop { + let slot = receiver.clone(); + let event = cx + .background_executor() + .spawn(async move { + let guard = slot.lock().ok()?; + Some( + match guard.recv_timeout(std::time::Duration::from_secs(1)) { + Ok(event) => Ok(event), + Err(error) => Err(error), + }, + ) + }) + .await; + let event = match event { + Some(Ok(event)) => event, + Some(Err(mpsc::RecvTimeoutError::Timeout)) => continue, + Some(Err(mpsc::RecvTimeoutError::Disconnected)) | None => break, + }; + let finished = matches!(&event, ProcessEvent::Finished { .. }); + let _ = this.update(cx, |view, cx| { + if view.maven_seq != seq { + return; + } + match event { + ProcessEvent::Started { label, .. } => { + push_run_line(&mut view.maven_output, label.clone()); + view.record_run(&label, cx); + } + ProcessEvent::Output { text, .. } => { + push_run_line(&mut view.maven_output, text); + } + ProcessEvent::Finished { + exit_code, + cancelled, + error, + } => { + if let Some(error) = error { + push_run_line(&mut view.maven_output, error); + } + if let Some(code) = exit_code { + push_run_line( + &mut view.maven_output, + format!( + "{}:{}", + crate::i18n::menu_text(cx, "run.exited"), + code + ), + ); + } else if cancelled { + push_run_line( + &mut view.maven_output, + crate::i18n::menu_text(cx, "run.finished").to_string(), + ); + } + view.maven_running = false; + } + } + cx.notify(); + }); + if finished { + break; + } + } + let _ = handle.join(); + }) + .detach(); + } + + /// 重跑上次 Maven 目标(头部重跑按钮,对齐 Tauri `rerunLastTest` 语义的子集: + /// Linux 无测试会话跟踪,重跑即以上次参数再跑一次)。 + pub fn rerun_maven(&mut self, cx: &mut Context) { + if self.maven_running { + return; + } + if let Some(last) = self.last_maven_goal.clone() { + self.run_maven_goal( + &last.pom_path, + &last.goal, + &last.target, + &last.profiles, + last.skip_tests, + cx, + ); + } + } + + /// 判断配置是否必须由 JDT/Core 准备项目运行时路径。 + pub fn needs_java_project_preparation(item: &RunConfigItem) -> bool { + let has_target = item + .source + .as_deref() + .is_some_and(|source| !source.trim().is_empty()) + && item + .main_class + .as_deref() + .is_some_and(|main| !main.trim().is_empty()); + has_target + && (item.toolchains.contains_key("maven") + || item.provider == "spring-boot.maven" + || item.provider == "maven.module") + && (item.provider == "java.main" || item.provider == "spring-boot.maven") + } + + fn is_current_run_execution(&self, reload_seq: u64, execution_seq: u64) -> bool { + sequence_is_current(self.run_seq, reload_seq) + && sequence_is_current(self.run_execution_seq, execution_seq) + } + + /// 当前选中项只按稳定 id 查找,不在 reload 后回退到旧列表。 + fn selected_run_item(&self) -> Option { + self.selected_run_config + .as_deref() + .and_then(|id| self.run_configs.iter().find(|item| item.id == id).cloned()) + } + + /// 重新加载 Git 提交记录。core 的 `git.historyPage` 需经宿主异步接线, + /// 这里用 `git -C log --oneline -50` 同步退化实现, + /// 解析 hash + 首行 message 只读展示。 + pub fn refresh_git_log(&mut self, cx: &mut Context) { + match load_git_log(&self.working_dir) { + Ok(entries) => { + self.git_log = entries; + self.git_log_error = None; + } + Err(err) => { + self.git_log.clear(); + self.git_log_error = Some(err); + } + } + cx.notify(); + } + + /// 占位日志入口:Tauri 没有通用 Output 面板,各业务接线后改走各自面板, + /// 当前仅保留调用点可编译,不存储不展示。 + pub fn append_log(&mut self, _log: String, _cx: &mut Context) {} + + /// 窗格通用头部:图标 + 标题 + 状态区 + 操作按钮 + 最小化, + /// 对齐 Tauri 各窗格自带按钮头(底部无标签切换条,切换只走左侧活动栏)。 + fn render_pane_header( + &self, + icon: IconName, + title: String, + status: Option, + buttons: Vec, + cx: &mut Context, + ) -> AnyElement { + h_flex() + .h(px(30.0)) + .w_full() + .flex_shrink_0() + .bg(ThemeColors::bg_tab_bar()) + .border_b_1() + .border_color(ThemeColors::border()) + .items_center() + .gap_1() + .px_2() + .child( + Icon::new(icon) + .size(px(14.0)) + .text_color(ThemeColors::text_muted()), + ) + .child( + div() + .flex_1() + .truncate() + .text_xs() + .font_weight(FontWeight::BOLD) + .text_color(ThemeColors::text_primary()) + .child(title), + ) + .when_some(status, |el, status| el.child(status)) + .children(buttons) + .child( + Button::new("pane-minimize") + .small() + .ghost() + .icon(IconName::Minus) + .tooltip(crate::i18n::menu_text(cx, "run.minimize")) + .on_click(cx.listener(|this, _event, _window, cx| { + this.toggle_collapsed(cx); + })), + ) + .into_any_element() + } + + /// 最小化之外的头部操作按钮(小幽灵图标按钮 + tooltip)。 + fn header_button( + id: String, + icon: IconName, + tooltip: String, + disabled: bool, + cx: &mut Context, + on_click: impl Fn(&mut Self, &mut Window, &mut Context) + 'static, + ) -> AnyElement { + Button::new(id) + .small() + .ghost() + .icon(icon) + .tooltip(tooltip) + .disabled(disabled) + .on_click(cx.listener(move |this, _event, window, cx| { + on_click(this, window, cx); + })) + .into_any_element() + } + + /// Run 窗格头部:运行/停止 + 重扫 + 跟随末尾 + 清空 + 最小化 + ///(对齐 Tauri RunPane 头)。 + fn render_run_header(&self, cx: &mut Context) -> AnyElement { + let running = self.run_running; + let status = running.then(|| { + div() + .text_xs() + .text_color(ThemeColors::accent_green()) + .child(crate::i18n::menu_text(cx, "run.running")) + .into_any_element() + }); + let title = format!( + "{} {}", + crate::i18n::menu_text(cx, "run.title"), + self.run_project_name + ); + let can_run = !matches!(self.run_state, RunProjectState::Loading); + let follow_end = self.run_follow_end; + let mut buttons = vec![ + Self::header_button( + "run-toggle".to_string(), + if running { + IconName::Square + } else { + IconName::Play + }, + crate::i18n::menu_text(cx, if running { "run.stop" } else { "run.run" }) + .to_string(), + !can_run, + cx, + |this, _window, cx| this.run_selected_config(cx), + ), + Self::header_button( + "run-rescan".to_string(), + IconName::RotateCw, + crate::i18n::menu_text(cx, "run.rescan").to_string(), + running, + cx, + |this, _window, cx| this.reload_run_project(cx), + ), + ]; + buttons.push( + Button::new("run-follow-end") + .small() + .when(follow_end, |b| b.primary()) + .when(!follow_end, |b| b.ghost()) + .icon(IconName::ArrowDownToLine) + .tooltip(crate::i18n::menu_text(cx, "run.scrollToEnd")) + .on_click(cx.listener(|this, _event, _window, cx| { + this.run_follow_end = !this.run_follow_end; + crate::settings::update(cx, |s| { + s.run_scroll_to_end = this.run_follow_end; + }); + // 打开跟随即滚到底(长度归零触发下次 render 滚动)。 + this.run_followed_len = 0; + cx.notify(); + })) + .into_any_element(), + ); + buttons.push(Self::header_button( + "run-clear-output".to_string(), + IconName::Trash, + crate::i18n::menu_text(cx, "run.clearOutput").to_string(), + self.run_output.is_empty(), + cx, + |this, _window, cx| { + this.run_output.clear(); + cx.notify(); + }, + )); + self.render_pane_header(IconName::Play, title, status, buttons, cx) + } + + /// Maven 窗格头部:停止 + 重跑 + 清空 + 最小化(对齐 Tauri MavenRunPane 头)。 + fn render_maven_header(&self, cx: &mut Context) -> AnyElement { + let title = match self.maven_title.clone() { + Some(task) => format!( + "{} - {} - {task}", + crate::i18n::menu_text(cx, "run.title"), + crate::i18n::menu_text(cx, "maven.title") + ), + None => format!( + "{} - {}", + crate::i18n::menu_text(cx, "run.title"), + crate::i18n::menu_text(cx, "maven.title") + ), + }; + let status = self.maven_running.then(|| { + div() + .text_xs() + .text_color(ThemeColors::accent_green()) + .child(crate::i18n::menu_text(cx, "run.running")) + .into_any_element() + }); + let can_rerun = self.last_maven_goal.is_some() && !self.maven_running; + let can_clear = !self.maven_output.is_empty() || self.maven_title.is_some(); + self.render_pane_header( + IconName::Box, + title, + status, + vec![ + Self::header_button( + "maven-stop".to_string(), + IconName::Square, + crate::i18n::menu_text(cx, "maven.stop").to_string(), + !self.maven_running, + cx, + |this, _window, cx| this.stop_running(cx), + ), + Self::header_button( + "maven-rerun".to_string(), + IconName::RefreshCw, + crate::i18n::menu_text(cx, "maven.rerunTest").to_string(), + !can_rerun, + cx, + |this, _window, cx| this.rerun_maven(cx), + ), + Self::header_button( + "maven-clear-output".to_string(), + IconName::Trash, + crate::i18n::menu_text(cx, "maven.clearOutput").to_string(), + !can_clear, + cx, + |this, _window, cx| { + this.maven_output.clear(); + cx.notify(); + }, + ), + ], + cx, + ) + } + + /// Run 面板体:配置列表 + 输出区;头部由 `render_run_header` 负责。 + fn render_run_panel(&mut self, cx: &mut Context) -> AnyElement { + if self.run_configs.is_empty() { + match &self.run_state { + RunProjectState::Loading => { + return run_center_text(run_ui_text(cx, "加载中…", "Loading…")); + } + RunProjectState::Failed(err) => { + let msg = if err.is_empty() { + run_ui_text(cx, "加载失败", "Failed to load") + } else { + err.clone() + }; + return v_flex() + .size_full() + .items_center() + .justify_center() + .gap_2() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(div().child(msg)) + .child( + Button::new("run-retry") + .small() + .ghost() + .label(run_ui_text(cx, "重试", "Retry")) + .on_click(cx.listener(|this, _event, _window, cx| { + this.reload_run_project(cx); + })), + ) + .into_any_element(); + } + _ => { + // Missing 空态 + 生成配置按钮。 + return v_flex() + .size_full() + .items_center() + .justify_center() + .gap_2() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(div().child(run_ui_text( + cx, + "暂无可运行配置", + "No runnable configurations", + ))) + .child( + Button::new("run-generate") + .small() + .primary() + .label(run_ui_text(cx, "生成配置", "Generate")) + .on_click(cx.listener(|this, _event, _window, cx| { + this.reload_run_project(cx); + })), + ) + .into_any_element(); + } + } + } + + let diagnostic_rows: Vec = self + .run_diagnostics + .iter() + .map(|message| { + div() + .px_2() + .py_1() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(message.clone()) + .into_any_element() + }) + .collect(); + let rows: Vec = self + .run_configs + .iter() + .map(|item| { + let id = item.id.clone(); + let run_id = item.id.clone(); + let selected = self.selected_run_config.as_deref() == Some(item.id.as_str()); + h_flex() + .id(format!("run-config-{}", item.id)) + .w_full() + .items_center() + .gap_1p5() + .px_2() + .py_1() + .rounded_sm() + .cursor_pointer() + .text_xs() + .when(selected, |row| { + row.bg(ThemeColors::bg_tab_hover()) + .text_color(ThemeColors::text_primary()) + }) + .when(!selected, |row| { + row.text_color(ThemeColors::text_muted()).hover(|h| { + h.bg(ThemeColors::bg_tab_hover()) + .text_color(ThemeColors::text_primary()) + }) + }) + .child( + v_flex() + .flex_1() + .min_w_0() + .child(div().truncate().child(item.name.clone())) + .child( + div() + .truncate() + .font_family("monospace") + .child(format!("{} {}", item.kind, item.detail)), + ), + ) + .child( + Button::new(format!("run-start-{}", item.id)) + .small() + .ghost() + .icon(IconName::Play) + .tooltip(run_ui_text(cx, "运行", "Run")) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.run_configuration_id(run_id.clone(), cx); + })), + ) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.selected_run_config = Some(id.clone()); + cx.notify(); + })) + .into_any_element() + }) + .collect(); + + let total = self.run_output.len(); + let start = total.saturating_sub(800); + let output: Vec = if self.run_output.is_empty() { + vec![div() + .text_color(ThemeColors::text_muted()) + .child(crate::i18n::menu_text(cx, "run.emptyOutput")) + .into_any_element()] + } else { + self.run_output[start..] + .iter() + .map(|line| render_output_line(line)) + .collect() + }; + // 跟随末尾:只在新增输出时滚到最后一行(对齐 Tauri `useFollowOutputEnd`)。 + if self.run_follow_end && output.len() != self.run_followed_len { + self.run_followed_len = output.len(); + if output.len() > 1 { + self.run_scroll.scroll_to_item(output.len() - 1); + } + } + + v_flex() + .size_full() + .child( + h_flex() + .flex_1() + .w_full() + .min_h_0() + .child( + div() + .flex_shrink_0() + .w(px(220.0)) + .h_full() + .overflow_y_scrollbar() + .border_r_1() + .border_color(ThemeColors::border()) + .py_1() + .children(rows) + .children(diagnostic_rows), + ) + .child( + div() + .flex_1() + .h_full() + .vertical_scrollbar(&self.run_scroll) + .p_2() + .font_family("monospace") + .text_xs() + .text_color(ThemeColors::text_primary()) + .children(output), + ), + ) + .into_any_element() + } + + /// Maven 面板体:输出区(首行 `$ mvn …`,流式追加);头部由 `render_maven_header` 负责。 + fn render_maven_panel(&self, cx: &mut Context) -> AnyElement { + let total = self.maven_output.len(); + let start = total.saturating_sub(800); + let output: Vec = if self.maven_output.is_empty() { + vec![div() + .text_color(ThemeColors::text_muted()) + .child(crate::i18n::menu_text(cx, "run.emptyOutput")) + .into_any_element()] + } else { + self.maven_output[start..] + .iter() + .map(|line| render_output_line(line)) + .collect() + }; + div() + .flex_1() + .w_full() + .min_h_0() + .overflow_y_scrollbar() + .p_2() + .font_family("monospace") + .text_xs() + .text_color(ThemeColors::text_primary()) + .children(output) + .into_any_element() + } + + /// GitLog 面板:只读提交列表,行点击不跳转;空态与失败文案兜底。 + fn render_git_log_panel(&self, cx: &mut Context) -> AnyElement { + if let Some(err) = self.git_log_error.clone() { + return div() + .size_full() + .flex() + .items_center() + .justify_center() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(if err.is_empty() { + git_log_failed_text(cx).to_string() + } else { + err + }) + .into_any_element(); + } + if self.git_log.is_empty() { + return div() + .size_full() + .flex() + .items_center() + .justify_center() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(git_log_empty_text(cx)) + .into_any_element(); + } + + let rows: Vec = self + .git_log + .iter() + .enumerate() + .map(|(idx, entry)| { + h_flex() + .id(format!("git-log-{idx}")) + .w_full() + .items_center() + .gap_2() + .px_3() + .py_1() + .text_xs() + .child( + div() + .flex_shrink_0() + .font_family("monospace") + .text_color(ThemeColors::accent_blue()) + .child(entry.hash.clone()), + ) + .child( + div() + .flex_1() + .truncate() + .text_color(ThemeColors::text_primary()) + .child(entry.message.clone()), + ) + .into_any_element() + }) + .collect(); + + div() + .flex_1() + .w_full() + .overflow_y_scrollbar() + .py_1() + .children(rows) + .into_any_element() + } +} + +impl Render for BottomPanelView { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + if self.is_collapsed { + return div().h(px(0.0)); + } + + // 各窗格自带按钮头(对齐 Tauri:底部无标签切换条,切换只走左侧活动栏)。 + let (header, body) = match self.active_tab { + BottomTab::Terminal => ( + self.render_pane_header( + IconName::Terminal, + crate::i18n::menu_text(cx, "workbench.terminal").to_string(), + None, + vec![Self::header_button( + "terminal-clear".to_string(), + IconName::Trash, + crate::i18n::menu_text(cx, "ui.clear").to_string(), + false, + cx, + |this, _window, cx| { + let _ = this.terminal.update(cx, |t, cx| t.clear(cx)); + }, + )], + cx, + ), + div() + .size_full() + .child(self.terminal.clone()) + .into_any_element(), + ), + BottomTab::Run => (self.render_run_header(cx), self.render_run_panel(cx)), + BottomTab::Maven => (self.render_maven_header(cx), self.render_maven_panel(cx)), + BottomTab::Diagnostics => ( + self.render_pane_header( + IconName::TriangleAlert, + format!( + "{} ({})", + crate::i18n::menu_text(cx, "workbench.diagnostics"), + self.diagnostics.len() + ), + None, + vec![Self::header_button( + "diagnostics-clear".to_string(), + IconName::Trash, + crate::i18n::menu_text(cx, "ui.clear").to_string(), + self.diagnostics.is_empty(), + cx, + |this, _window, cx| { + this.diagnostics.clear(); + cx.emit(BottomPanelEvent::ClearDiagnostics); + cx.notify(); + }, + )], + cx, + ), + self.render_diagnostics_panel(cx), + ), + BottomTab::GitLog => ( + self.render_pane_header( + IconName::GitGraph, + crate::i18n::menu_text(cx, "workbench.gitLog").to_string(), + None, + vec![Self::header_button( + "gitlog-refresh".to_string(), + IconName::RotateCw, + crate::i18n::menu_text(cx, "ui.refresh").to_string(), + false, + cx, + |this, _window, cx| this.refresh_git_log(cx), + )], + cx, + ), + self.render_git_log_panel(cx), + ), + }; + + v_flex() + .h(px(self.height)) + .w_full() + .bg(ThemeColors::bg_bottom_panel()) + .border_t_1() + .border_color(ThemeColors::border()) + .child(header) + .child(div().flex_1().w_full().min_h_0().child(body)) + } +} + +/// 同步取 Git 提交记录:`git -C log --oneline -N`,解析 hash + 首行 +/// message。core 侧 `git.historyPage` 存在但 linux `CoreClient` 尚未暴露对应 +/// 方法,且本次只允许改动底部面板/活动栏两文件,故先用子进程退化实现; +/// 宿主后续可把 [`BottomPanelView::refresh_git_log`] 切到 core 异步接线。 +fn load_git_log(workdir: &str) -> Result, String> { + let output = std::process::Command::new("git") + .arg("-C") + .arg(workdir) + .arg("log") + .arg("--oneline") + .arg(format!("-{MAX_GIT_LOG_ENTRIES}")) + .output() + .map_err(|e| e.to_string())?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + return Err(stderr); + } + let stdout = String::from_utf8_lossy(&output.stdout); + let entries = stdout + .lines() + .filter_map(|line| { + let line = line.trim(); + if line.is_empty() { + return None; + } + let (hash, message) = match line.split_once(char::is_whitespace) { + Some((hash, message)) => (hash.trim(), message.trim()), + None => (line, ""), + }; + if hash.is_empty() { + return None; + } + Some(GitLogEntry { + hash: hash.to_string(), + message: message.to_string(), + }) + }) + .collect(); + Ok(entries) +} + +fn workspace_relative_path(root: &str, path: &str) -> Option { + let root = std::path::Path::new(root); + let path = std::path::Path::new(path); + let relative = if path.is_absolute() { + path.strip_prefix(root).ok()? + } else { + path + }; + Some(relative.to_string_lossy().replace('\\', "/")) +} + +/// pom 所在目录相对 root 的模块路径:根 pom 为 `.`。 +fn maven_module_for_pom(root: &str, pom: &str) -> String { + let relative = std::path::Path::new(pom) + .parent() + .and_then(|directory| directory.strip_prefix(root).ok()) + .map(|relative| relative.to_string_lossy().replace('\\', "/")) + .unwrap_or_default(); + let trimmed = relative.trim_matches('/').to_string(); + if trimmed.is_empty() { + ".".to_string() + } else { + trimmed + } +} + +/// 行首空白转不换行空格:GPUI `Normal` 会塌缩连续空白(Tauri `pre-wrap` +/// 保留),只转行首以兼顾缩进保留与行内换行。 +fn preserve_leading_whitespace(line: &str) -> String { + let indent_len = line.len() - line.trim_start_matches([' ', '\t']).len(); + let (indent, rest) = line.split_at(indent_len); + let mut out = String::with_capacity(line.len() + indent.len() * 2); + for c in indent.chars() { + if c == '\t' { + out.push_str("\u{a0}\u{a0}\u{a0}\u{a0}"); + } else { + out.push('\u{a0}'); + } + } + out.push_str(rest); + out +} + +/// 输出行渲染:显式换行模式(对齐 Tauri `pre-wrap`)+ 行首缩进保留。 +fn render_output_line(line: &str) -> AnyElement { + div() + .whitespace_normal() + .child(preserve_leading_whitespace(line)) + .into_any_element() +} +fn push_run_line(output: &mut Vec, line: String) { + output.push(line); + if output.len() > MAX_RUN_OUTPUT { + let overflow = output.len() - MAX_RUN_OUTPUT; + output.drain(..overflow); + } +} + +fn run_center_text(text: String) -> AnyElement { + div() + .size_full() + .flex() + .items_center() + .justify_center() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(text) + .into_any_element() +} + +fn run_ui_text(cx: &gpui_kit::App, zh: &str, en: &str) -> String { + if crate::i18n::is_zh(cx) { + zh.to_string() + } else { + en.to_string() + } +} + +fn git_log_empty_text(cx: &gpui_kit::App) -> &'static str { + if crate::i18n::is_zh(cx) { + "暂无提交记录" + } else { + "No commits" + } +} + +fn git_log_failed_text(cx: &gpui_kit::App) -> &'static str { + if crate::i18n::is_zh(cx) { + "加载提交记录失败" + } else { + "Failed to load git log" + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn item( + provider: &str, + source: Option<&str>, + main_class: Option<&str>, + module: Option<&str>, + ) -> RunConfigItem { + let mut extensions = serde_json::Map::new(); + if let Some(source) = source { + extensions.insert("java".to_string(), serde_json::json!({ "source": source })); + } + let mut maven = serde_json::Map::new(); + if let Some(module) = module { + maven.insert("module".to_string(), serde_json::json!(module)); + } + if let Some(main_class) = main_class { + maven.insert("mainClass".to_string(), serde_json::json!(main_class)); + } + extensions.insert("maven".to_string(), serde_json::Value::Object(maven)); + let mut toolchains = serde_json::Map::new(); + toolchains.insert("java".to_string(), serde_json::json!("project-jdk")); + if module.is_some() || provider.contains("maven") { + toolchains.insert("maven".to_string(), serde_json::json!("project-maven")); + } + let value = serde_json::json!({ + "id": "test", + "name": "Test", + "provider": provider, + "toolchains": toolchains, + "extensions": extensions, + }); + RunConfigItem::from_value(&value).expect("run config") + } + + #[test] + fn only_maven_backed_java_and_spring_targets_require_jdt() { + assert!(BottomPanelView::needs_java_project_preparation(&item( + "java.main", + Some("src/App.java"), + Some("demo.App"), + Some(".") + ))); + assert!(BottomPanelView::needs_java_project_preparation(&item( + "spring-boot.maven", + Some("src/App.java"), + Some("demo.App"), + Some(".") + ))); + assert!(!BottomPanelView::needs_java_project_preparation(&item( + "quarkus.maven", + Some("src/App.java"), + Some("demo.App"), + Some(".") + ))); + assert!(!BottomPanelView::needs_java_project_preparation(&item( + "java.main", + Some("src/App.java"), + Some("demo.App"), + None + ))); + assert!(!BottomPanelView::needs_java_project_preparation(&item( + "java.main", + None, + Some("demo.App"), + Some(".") + ))); + + let standalone = RunConfigItem::from_value(&serde_json::json!({ + "id": "standalone", + "name": "Standalone", + "provider": "java.main", + "toolchains": {"java": "project-jdk"}, + "extensions": { + "java": {"source": "src/App.java"}, + "maven": {"module": ".", "mainClass": "demo.App"} + } + })) + .expect("standalone config"); + assert!(!BottomPanelView::needs_java_project_preparation( + &standalone + )); + } +} diff --git a/linux/src/workbench/branch_manager.rs b/linux/src/workbench/branch_manager.rs new file mode 100644 index 000000000..36dc3cad1 --- /dev/null +++ b/linux/src/workbench/branch_manager.rs @@ -0,0 +1,874 @@ +//! 分支管理器弹窗:对齐 Tauri `GitBranchManager`(`GitCommandSurface` 弹窗)。 +//! +//! - 顶部三个 tab(仓库 / 分支 / 工作树),按 tab 切换搜索框占位与计数。 +//! - 分支行点击经 core 直连 `git.write` 检出,当前分支打勾; +//! 搜索串无精确匹配时首行可建分支(`createBranch` 后再 `checkout` 切过去)。 +//! - 工作树行点击把路径交给父级(`OpenWorktree`),由父级打开对应项目。 +//! - 成功统一发射 [`BranchManagerEvent::CheckoutDone`](父级刷新+关弹窗); +//! 失败只在弹窗内显示错误行,不发射。 +//! - 遮罩 + 卡片布局与键盘输入仿 `quick_open.rs`;core 调用模式照抄 +//! `sidebar.rs`(`CoreClient` + `cx.spawn`)与 `view.rs` 的分支解析。 + +use gpui_kit::assets::IconName; +use gpui_kit::component::button::{Button, ButtonVariants as _}; +use gpui_kit::component::scroll::ScrollableElement as _; +use gpui_kit::component::{h_flex, v_flex, Icon, Selectable as _, Sizable as _}; +use gpui_kit::prelude::FluentBuilder as _; +use gpui_kit::{ + div, px, rgba, Context, EventEmitter, FocusHandle, FontWeight, InteractiveElement as _, + IntoElement, KeyDownEvent, ParentElement as _, Render, StatefulInteractiveElement as _, + Styled as _, Window, +}; + +use crate::core::CoreClient; +use crate::theme::ThemeColors; + +/// 管理器顶部的三个分区(对齐 Tauri `CommandTabs`)。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BranchTab { + Repositories, + Branches, + Worktrees, +} + +impl BranchTab { + /// 分区标题文案键(与 Tauri `git.repositories/branches/worktrees` 对齐)。 + fn title_key(self) -> &'static str { + match self { + BranchTab::Repositories => "git.repositories", + BranchTab::Branches => "git.branches", + BranchTab::Worktrees => "git.worktrees", + } + } + + /// 搜索框占位文案键(按 tab 切换)。 + fn placeholder_key(self) -> &'static str { + match self { + BranchTab::Repositories => "git.filterRepositories", + BranchTab::Branches => "git.searchBranches", + BranchTab::Worktrees => "git.searchWorktrees", + } + } +} + +/// 分支管理器对外事件。 +#[derive(Debug, Clone)] +pub enum BranchManagerEvent { + /// 检出或建分支成功:父级负责刷新 git 状态并关闭弹窗。 + CheckoutDone, + /// 请求打开指定工作树路径的项目(父级关弹窗后打开)。 + OpenWorktree(String), + /// 请求关闭弹窗(遮罩点击 / Esc)。 + Close, +} + +/// 工作树列表项(字段形状对齐 core `GitWorktreeResponse` 的 camelCase 返回)。 +#[derive(Debug, Clone)] +pub struct WorktreeInfo { + pub path: String, + pub branch: Option, + pub is_current: bool, +} + +/// 居中的分支管理器弹窗(宽 560、高 420,仿 `project_dialog` 卡片)。 +pub struct BranchManagerView { + repo_path: String, + current_branch: Option, + active_tab: BranchTab, + query: String, + branches: Vec, + worktrees: Vec, + error: Option, + busy: bool, + focus_handle: FocusHandle, + client: CoreClient, +} + +impl EventEmitter for BranchManagerView {} + +impl BranchManagerView { + pub fn new(repo_path: String, cx: &mut Context) -> Self { + let mut view = Self { + repo_path, + current_branch: None, + active_tab: BranchTab::Branches, + query: String::new(), + branches: Vec::new(), + worktrees: Vec::new(), + error: None, + busy: false, + focus_handle: cx.focus_handle(), + client: CoreClient::new(), + }; + view.reload(cx); + view + } + + /// 更新仓库并重载分支与工作树(打开弹窗时由父级调用)。 + pub fn set_repo(&mut self, path: String, current: Option, cx: &mut Context) { + self.repo_path = path; + self.current_branch = current; + self.query.clear(); + self.error = None; + self.busy = false; + self.reload(cx); + } + + /// 并发拉取 `git.references` 与 `worktrees`,分支排序去重。 + pub fn reload(&mut self, cx: &mut Context) { + let client = self.client.clone(); + let root = self.repo_path.clone(); + let root_wt = self.repo_path.clone(); + cx.spawn(async move |this, cx| { + let refs_task = client.execute::( + &cx, + "git.references", + serde_json::json!({ "root": root }), + ); + let wt_task = client.execute::( + &cx, + "worktrees", + serde_json::json!({ "root": root_wt }), + ); + let refs_val = refs_task.await.unwrap_or(serde_json::Value::Null); + let wt_val = wt_task.await.unwrap_or(serde_json::Value::Null); + // 分支解析照抄 view.rs observer:kind == local 的 shortName。 + let mut branches: Vec = refs_val + .get("references") + .and_then(|r| r.as_array()) + .map(|arr| { + arr.iter() + .filter(|item| item.get("kind").and_then(|k| k.as_str()) == Some("local")) + .filter_map(|item| { + item.get("shortName") + .and_then(|n| n.as_str()) + .map(|s| s.to_string()) + }) + .collect() + }) + .unwrap_or_default(); + branches.sort(); + branches.dedup(); + let worktrees: Vec = wt_val + .get("worktrees") + .and_then(|w| w.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|item| { + let path = item + .get("path") + .and_then(|p| p.as_str()) + .unwrap_or("") + .to_string(); + if path.is_empty() { + return None; + } + Some(WorktreeInfo { + path, + branch: item + .get("branch") + .and_then(|b| b.as_str()) + .map(|s| s.to_string()), + is_current: item + .get("isCurrent") + .and_then(|b| b.as_bool()) + .unwrap_or(false), + }) + }) + .collect() + }) + .unwrap_or_default(); + let _ = this.update(cx, |view, cx| { + view.branches = branches; + view.worktrees = worktrees; + view.error = None; + cx.notify(); + }); + }) + .detach(); + } + + /// 检出指定本地分支:`git.write` 成功即 `CheckoutDone`,失败只显示错误行。 + fn checkout(&mut self, branch: String, cx: &mut Context) { + if self.busy || Some(branch.as_str()) == self.current_branch.as_deref() { + return; + } + self.busy = true; + self.error = None; + cx.notify(); + let client = self.client.clone(); + let payload = git_write_payload( + &self.repo_path, + "checkout", + serde_json::json!({ + "reference": format!("refs/heads/{branch}"), + "referenceKind": "local", + }), + ); + cx.spawn(async move |this, cx| { + let task = + client.execute::(&cx, "git.write", payload); + match task.await { + Ok(_) => { + let _ = this.update(cx, |view, cx| { + view.busy = false; + cx.emit(BranchManagerEvent::CheckoutDone); + }); + } + Err(err) => { + let _ = this.update(cx, |view, cx| { + view.busy = false; + view.error = Some(err); + cx.notify(); + }); + } + } + }) + .detach(); + } + + /// 建分支后切过去:`createBranch`(`reference: "HEAD"`)成功再调一次 checkout。 + fn create_and_checkout(&mut self, name: String, cx: &mut Context) { + let name = name.trim().to_string(); + if self.busy || name.is_empty() { + return; + } + self.busy = true; + self.error = None; + cx.notify(); + let client = self.client.clone(); + let root = self.repo_path.clone(); + let checkout_ref = format!("refs/heads/{name}"); + let create_payload = git_write_payload( + &root, + "createBranch", + serde_json::json!({ "name": name, "reference": "HEAD" }), + ); + let checkout_payload = git_write_payload( + &root, + "checkout", + serde_json::json!({ "reference": checkout_ref, "referenceKind": "local" }), + ); + cx.spawn(async move |this, cx| { + let create_task = client.execute::( + &cx, + "git.write", + create_payload, + ); + let create_result = create_task.await; + let final_result = match create_result { + Ok(_) => { + let checkout_task = client.execute::( + &cx, + "git.write", + checkout_payload, + ); + checkout_task.await.map(|_| ()) + } + Err(err) => Err(err), + }; + match final_result { + Ok(()) => { + let _ = this.update(cx, |view, cx| { + view.busy = false; + cx.emit(BranchManagerEvent::CheckoutDone); + }); + } + Err(err) => { + let _ = this.update(cx, |view, cx| { + view.busy = false; + view.error = Some(err); + cx.notify(); + }); + } + } + }) + .detach(); + } + + /// 当前 tab 下按 query 过滤后的分支(大小写不敏感包含)。 + fn filtered_branches(&self) -> Vec { + let q = self.query.trim().to_lowercase(); + self.branches + .iter() + .filter(|b| q.is_empty() || b.to_lowercase().contains(&q)) + .cloned() + .collect() + } + + /// 当前 tab 下按 query 过滤后的工作树(按分支名或路径匹配)。 + fn filtered_worktrees(&self) -> Vec { + let q = self.query.trim().to_lowercase(); + self.worktrees + .iter() + .filter(|w| { + q.is_empty() + || w.path.to_lowercase().contains(&q) + || w.branch + .as_deref() + .unwrap_or("") + .to_lowercase() + .contains(&q) + }) + .cloned() + .collect() + } + + /// 当前仓库在 repositories tab 下是否匹配 query。 + fn repo_matches(&self) -> bool { + let q = self.query.trim().to_lowercase(); + q.is_empty() + || self.repo_path.to_lowercase().contains(&q) + || repo_name(&self.repo_path).to_lowercase().contains(&q) + } + + /// 计数行文案(`{count}` 占位在渲染时替换,单复数按英文区分键)。 + fn count_text(&self, cx: &gpui_kit::App) -> String { + let (key, count) = match self.active_tab { + BranchTab::Repositories => { + let n = usize::from(self.repo_matches()); + ( + if n == 1 { + "git.repositoryCount" + } else { + "git.repositoriesCount" + }, + n, + ) + } + BranchTab::Branches => { + let n = self.filtered_branches().len(); + ( + if n == 1 { + "git.branchCount" + } else { + "git.branchesCount" + }, + n, + ) + } + BranchTab::Worktrees => { + let n = self.filtered_worktrees().len(); + ( + if n == 1 { + "git.worktreeCount" + } else { + "git.worktreesCount" + }, + n, + ) + } + }; + crate::i18n::menu_text(cx, key).replace("{count}", &count.to_string()) + } +} + +/// 构造 `git.write` 全字段 camelCase payload(String 无值传 null,bool 传 false)。 +fn git_write_payload(root: &str, operation: &str, extra: serde_json::Value) -> serde_json::Value { + let mut payload = serde_json::json!({ + "root": root, + "operation": operation, + "paths": [], + "reference": null, + "gitReference": null, + "referenceKind": null, + "revision": null, + "name": null, + "message": null, + "remote": null, + "destination": null, + "mode": null, + "includeUntracked": false, + "checkout": false, + "amend": false, + "force": false, + "autoStash": false, + }); + if let (Some(obj), Some(patch)) = (payload.as_object_mut(), extra.as_object()) { + for (key, value) in patch { + obj.insert(key.clone(), value.clone()); + } + } + payload +} + +/// 仓库显示名:路径最后一段。 +fn repo_name(path: &str) -> String { + path.rsplit('/').next().unwrap_or(path).to_string() +} + +impl Render for BranchManagerView { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + window.focus(&self.focus_handle, cx); + + let active_tab = self.active_tab; + let placeholder: String = + crate::i18n::menu_text(cx, active_tab.placeholder_key()).to_string(); + let count_text = self.count_text(cx); + let busy = self.busy; + + // 分支 tab:精确匹配存在时不展示建分支行。 + let trimmed_query = self.query.trim().to_string(); + let has_exact_match = + !trimmed_query.is_empty() && self.branches.iter().any(|b| *b == trimmed_query); + let show_create_row = + active_tab == BranchTab::Branches && !trimmed_query.is_empty() && !has_exact_match; + let create_label = if show_create_row { + crate::i18n::menu_text(cx, "git.createNewBranch").replace("{name}", &trimmed_query) + } else { + String::new() + }; + + div() + .id("branch-manager-backdrop") + .track_focus(&self.focus_handle) + .absolute() + .inset_0() + .bg(rgba(0x00000088)) + .flex() + .items_center() + .justify_center() + .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { + let key = event.keystroke.key.as_str(); + match key { + "escape" => { + cx.emit(BranchManagerEvent::Close); + } + "backspace" => { + this.query.pop(); + this.error = None; + cx.notify(); + } + "space" => { + this.query.push(' '); + this.error = None; + cx.notify(); + } + _ => { + if !event.keystroke.modifiers.control + && !event.keystroke.modifiers.alt + && !event.keystroke.modifiers.platform + { + let mut changed = false; + if let Some(ch) = &event.keystroke.key_char { + this.query.push_str(ch); + changed = true; + } else if key.chars().count() == 1 { + this.query.push_str(key); + changed = true; + } + if changed { + this.error = None; + cx.notify(); + } + } + } + } + })) + .on_mouse_down( + gpui_kit::MouseButton::Left, + cx.listener(|_this, _event, _window, cx| { + cx.emit(BranchManagerEvent::Close); + }), + ) + .child( + v_flex() + .id("branch-manager-card") + .w(px(560.0)) + .h(px(420.0)) + .bg(ThemeColors::surface()) + .border_1() + .border_color(ThemeColors::border()) + .rounded_lg() + .shadow_lg() + .overflow_hidden() + .on_mouse_down( + gpui_kit::MouseButton::Left, + cx.listener(|_this, _event, _window, cx| { + cx.stop_propagation(); + }), + ) + .child(self.render_tabs(cx)) + .child(self.render_search_row(placeholder)) + .child( + div() + .w_full() + .px_4() + .py_1() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child(count_text), + ) + .child(self.render_list(show_create_row, create_label, busy, cx)) + .when(self.error.is_some(), |card| { + card.child( + div() + .w_full() + .px_4() + .py_2() + .border_t_1() + .border_color(ThemeColors::border()) + .text_xs() + .text_color(ThemeColors::destructive()) + .child(self.error.clone().unwrap_or_default()), + ) + }), + ) + } +} + +impl BranchManagerView { + /// 顶部三 tab 行(当前高亮)。 + fn render_tabs(&self, cx: &mut Context) -> impl IntoElement { + let tabs = [ + BranchTab::Repositories, + BranchTab::Branches, + BranchTab::Worktrees, + ]; + h_flex() + .w_full() + .items_center() + .gap_1() + .px_3() + .py_2() + .border_b_1() + .border_color(ThemeColors::border()) + .children(tabs.into_iter().enumerate().map(|(ix, tab)| { + let label: String = crate::i18n::menu_text(cx, tab.title_key()).to_string(); + let selected = tab == self.active_tab; + Button::new(format!("branch-manager-tab-{ix}")) + .small() + .ghost() + .label(label) + .selected(selected) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.active_tab = tab; + cx.notify(); + })) + })) + } + + /// 搜索行:图标 + 查询串/占位 + 静态光标(仿 quick_open 输入行)。 + fn render_search_row(&self, placeholder: String) -> impl IntoElement { + h_flex() + .h(px(48.0)) + .w_full() + .items_center() + .gap_2p5() + .px_4() + .border_b_1() + .border_color(ThemeColors::border()) + .child( + Icon::new(IconName::Search) + .size(px(16.0)) + .text_color(ThemeColors::primary()), + ) + .child( + h_flex() + .flex_1() + .items_center() + .gap_1() + .child( + div() + .text_sm() + .text_color(if self.query.is_empty() { + ThemeColors::subtle_foreground() + } else { + ThemeColors::foreground() + }) + .child(if self.query.is_empty() { + placeholder + } else { + self.query.clone() + }), + ) + .child(div().w(px(2.0)).h(px(16.0)).bg(ThemeColors::primary())), + ) + } + + /// 按当前 tab 渲染滚动列表。 + fn render_list( + &self, + show_create_row: bool, + create_label: String, + busy: bool, + cx: &mut Context, + ) -> impl IntoElement { + let tab = self.active_tab; + let query_empty = self.query.trim().is_empty(); + div() + .flex_1() + .w_full() + .overflow_y_scrollbar() + .py_1() + .when(tab == BranchTab::Branches, |list| { + let filtered = self.filtered_branches(); + let empty_key = if query_empty { + "git.noBranchesFound" + } else { + "git.noMatchingBranches" + }; + let empty_text: String = crate::i18n::menu_text(cx, empty_key).to_string(); + let current = self.current_branch.clone(); + list.when(show_create_row, |list| { + list.child(create_branch_row(create_label, busy, cx)) + }) + .when(filtered.is_empty(), |list| { + list.child(empty_row(empty_text)) + }) + .children( + filtered + .into_iter() + .enumerate() + .map(|(ix, name)| branch_row(ix, name, current.clone(), busy, cx)), + ) + }) + .when(tab == BranchTab::Worktrees, |list| { + let filtered = self.filtered_worktrees(); + let empty_text: String = + crate::i18n::menu_text(cx, "git.noMatchingBranches").to_string(); + list.when(filtered.is_empty(), |list| { + list.child(empty_row(empty_text)) + }) + .children( + filtered + .into_iter() + .enumerate() + .map(|(ix, info)| worktree_row(ix, info, busy, cx)), + ) + }) + .when(tab == BranchTab::Repositories, |list| { + if self.repo_matches() { + list.child(repository_row( + repo_name(&self.repo_path), + self.repo_path.clone(), + )) + } else { + let empty_text: String = + crate::i18n::menu_text(cx, "git.noMatchingBranches").to_string(); + list.child(empty_row(empty_text)) + } + }) + } +} + +/// 建分支首行(点击从搜索串建分支并切过去)。 +fn create_branch_row( + label: String, + busy: bool, + cx: &mut Context, +) -> impl IntoElement { + h_flex() + .id("branch-manager-create") + .h(px(32.0)) + .w_full() + .mx_2() + .px_2p5() + .items_center() + .gap_2() + .rounded_md() + .cursor_pointer() + .hover(|h| h.bg(ThemeColors::accent())) + .child( + Icon::new(IconName::Plus) + .size(px(14.0)) + .text_color(if busy { + ThemeColors::muted_foreground() + } else { + ThemeColors::primary() + }), + ) + .child( + div() + .flex_1() + .text_sm() + .font_weight(FontWeight::MEDIUM) + .text_color(ThemeColors::foreground()) + .child(label), + ) + .on_click(cx.listener(|this, _event, _window, cx| { + if this.busy { + return; + } + let name = this.query.trim().to_string(); + this.create_and_checkout(name, cx); + })) + .into_any_element() +} + +/// 分支行:名 + 当前分支打勾,点击检出。 +fn branch_row( + ix: usize, + name: String, + current: Option, + busy: bool, + cx: &mut Context, +) -> gpui_kit::AnyElement { + let is_current = Some(name.as_str()) == current.as_deref(); + let target = name.clone(); + h_flex() + .id(("branch-manager-branch", ix)) + .h(px(32.0)) + .w_full() + .mx_2() + .px_2p5() + .items_center() + .gap_2() + .rounded_md() + .cursor_pointer() + .hover(|h| h.bg(ThemeColors::accent())) + .child( + Icon::new(IconName::GitBranch) + .size(px(14.0)) + .text_color(if is_current { + ThemeColors::primary() + } else { + ThemeColors::muted_foreground() + }), + ) + .child( + div() + .flex_1() + .truncate() + .text_sm() + .font_weight(FontWeight::MEDIUM) + .text_color(ThemeColors::foreground()) + .child(name), + ) + .when(is_current, |row| { + row.child( + Icon::new(IconName::Check) + .size(px(14.0)) + .text_color(ThemeColors::primary()), + ) + }) + .on_click(cx.listener(move |this, _event, _window, cx| { + if busy { + return; + } + this.checkout(target.clone(), cx); + })) + .into_any_element() +} + +/// 工作树行:分支名(无分支显示路径)+ 路径副行 + 当前打勾,点击打开对应项目。 +fn worktree_row( + ix: usize, + info: WorktreeInfo, + busy: bool, + cx: &mut Context, +) -> gpui_kit::AnyElement { + let path = info.path.clone(); + let title = info.branch.clone().unwrap_or_else(|| info.path.clone()); + let is_current = info.is_current; + h_flex() + .id(("branch-manager-worktree", ix)) + .h(px(40.0)) + .w_full() + .mx_2() + .px_2p5() + .items_center() + .gap_2() + .rounded_md() + .cursor_pointer() + .hover(|h| h.bg(ThemeColors::accent())) + .child( + Icon::new(IconName::Folder) + .size(px(14.0)) + .text_color(ThemeColors::muted_foreground()), + ) + .child( + v_flex() + .flex_1() + .min_w_0() + .child( + div() + .w_full() + .truncate() + .text_sm() + .font_weight(FontWeight::MEDIUM) + .text_color(ThemeColors::foreground()) + .child(title), + ) + .child( + div() + .w_full() + .truncate() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child(info.path.clone()), + ), + ) + .when(is_current, |row| { + row.child( + Icon::new(IconName::Check) + .size(px(14.0)) + .text_color(ThemeColors::primary()), + ) + }) + .on_click(cx.listener(move |_this, _event, _window, cx| { + if busy { + return; + } + cx.emit(BranchManagerEvent::OpenWorktree(path.clone())); + })) + .into_any_element() +} + +/// 仓库行:当前仓库名 + 路径 + 打勾,点击无动作。 +fn repository_row(name: String, path: String) -> gpui_kit::AnyElement { + h_flex() + .id("branch-manager-repo") + .h(px(40.0)) + .w_full() + .mx_2() + .px_2p5() + .items_center() + .gap_2() + .rounded_md() + .child( + Icon::new(IconName::Folder) + .size(px(14.0)) + .text_color(ThemeColors::primary()), + ) + .child( + v_flex() + .flex_1() + .min_w_0() + .child( + div() + .w_full() + .truncate() + .text_sm() + .font_weight(FontWeight::MEDIUM) + .text_color(ThemeColors::foreground()) + .child(name), + ) + .child( + div() + .w_full() + .truncate() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child(path), + ), + ) + .child( + Icon::new(IconName::Check) + .size(px(14.0)) + .text_color(ThemeColors::primary()), + ) + .into_any_element() +} + +/// 空态行。 +fn empty_row(text: String) -> gpui_kit::AnyElement { + div() + .w_full() + .py_8() + .text_center() + .text_sm() + .text_color(ThemeColors::subtle_foreground()) + .child(text) + .into_any_element() +} diff --git a/linux/src/workbench/command_palette.rs b/linux/src/workbench/command_palette.rs new file mode 100644 index 000000000..7bcb40e00 --- /dev/null +++ b/linux/src/workbench/command_palette.rs @@ -0,0 +1,914 @@ +//! 工作台命令面板(Command Palette)模态视图。 +//! +//! 复刻 Tauri 端 `features/command-palette` 的通用弹层壳与动作清单:顶部搜索输入行、 +//! 按 category 分组的命令列表、底部操作指引。当前只提供点击与键盘交互,选中后通过 +//! [`CommandPaletteEvent::Execute`] 把命令 id 交给上层工作台处理,不在本模块执行真实命令。 + +use gpui_kit::assets::IconName; +use gpui_kit::component::scroll::ScrollableElement as _; +use gpui_kit::component::{h_flex, v_flex, Icon}; +use gpui_kit::prelude::FluentBuilder as _; +use gpui_kit::{ + div, px, rgba, AnyElement, Context, EventEmitter, FocusHandle, FontWeight, + InteractiveElement as _, IntoElement, KeyDownEvent, ParentElement as _, Render, + StatefulInteractiveElement as _, Styled as _, Window, +}; + +use crate::theme::ThemeColors; + +/// 命令面板中的一条命令。 +/// +/// `id` 为稳定的命令标识(如 `file.save`),`category` 用作列表分组标题, +/// `shortcut` 为展示用快捷键文案(不参与按键分发)。 +#[derive(Debug, Clone)] +pub struct CommandPaletteItem { + pub id: String, + pub name: String, + pub category: String, + pub shortcut: Option, + pub icon: IconName, +} + +/// 命令面板对外事件。 +#[derive(Debug, Clone)] +pub enum CommandPaletteEvent { + /// 选中并确认执行某条命令,携带命令 id。 + Execute(String), + /// 请求关闭命令面板。 + Close, +} + +/// 列表渲染行:分组标题或命令项(`Item` 携带该项在 `filtered` 中的位置)。 +enum PaletteRow { + Header(String), + Item(usize), +} + +/// 居中的命令面板模态,宽 704、最大高 440。 +pub struct CommandPaletteModal { + pub query: String, + pub items: Vec, + /// 命中筛选的命令在 `items` 中的下标,保持构造时的分组顺序。 + pub filtered: Vec, + pub selected_index: usize, + pub focus_handle: FocusHandle, + /// 预留的多级视图栈,根视图为 `"root"`。 + pub view_stack: Vec, +} + +impl EventEmitter for CommandPaletteModal {} + +impl CommandPaletteModal { + pub fn new(cx: &mut Context) -> Self { + let items = build_commands(); + let filtered = (0..items.len()).collect(); + + Self { + query: String::new(), + items, + filtered, + selected_index: 0, + focus_handle: cx.focus_handle(), + view_stack: vec!["root".to_string()], + } + } + + /// 复位到初始状态:清空查询、选中归零、视图栈回到根,并重算筛选结果。 + pub fn reset(&mut self, cx: &mut Context) { + self.query.clear(); + self.selected_index = 0; + self.view_stack = vec!["root".to_string()]; + self.recompute_filtered(); + cx.notify(); + } + + /// 直接设置查询串(供上层联动外部输入)。 + #[allow(dead_code)] + pub fn set_query(&mut self, q: impl Into, cx: &mut Context) { + self.query = q.into(); + self.selected_index = 0; + self.recompute_filtered(); + cx.notify(); + } + + /// 在当前命中列表内移动选中项,`delta` 为正向下、为负向上,越界即夹紧。 + pub fn move_selection(&mut self, delta: i32, cx: &mut Context) { + let total = self.filtered.len(); + if total == 0 { + self.selected_index = 0; + } else { + let current = self.selected_index.min(total - 1) as i32; + self.selected_index = (current + delta).clamp(0, total as i32 - 1) as usize; + } + cx.notify(); + } + + /// 按查询串重算命中列表:命令名、分组、id 任一包含即命中。 + fn recompute_filtered(&mut self) { + let q = self.query.trim().to_lowercase(); + self.filtered = self + .items + .iter() + .enumerate() + .filter(|(_, item)| { + q.is_empty() + || item.name.to_lowercase().contains(&q) + || item.category.to_lowercase().contains(&q) + || item.id.to_lowercase().contains(&q) + }) + .map(|(idx, _)| idx) + .collect(); + if self.selected_index >= self.filtered.len() { + self.selected_index = self.filtered.len().saturating_sub(1); + } + } + + /// 把命中列表展开成“分组标题 + 命令项”的渲染行序列。 + fn build_rows(&self) -> Vec { + let mut rows = Vec::new(); + let mut last_category: Option<&str> = None; + for (position, &item_idx) in self.filtered.iter().enumerate() { + let item = &self.items[item_idx]; + if last_category != Some(item.category.as_str()) { + rows.push(PaletteRow::Header(item.category.clone())); + last_category = Some(item.category.as_str()); + } + rows.push(PaletteRow::Item(position)); + } + rows + } + + /// 当前有效选中下标(对空列表安全)。 + fn current_index(&self) -> usize { + if self.filtered.is_empty() { + 0 + } else { + self.selected_index.min(self.filtered.len() - 1) + } + } + + /// 执行命中列表第 `position` 项,发出其命令 id。 + fn execute_at(&self, position: usize, cx: &mut Context) { + if let Some(&item_idx) = self.filtered.get(position) { + cx.emit(CommandPaletteEvent::Execute( + self.items[item_idx].id.clone(), + )); + } + } +} + +impl Render for CommandPaletteModal { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + // 请求聚焦以接收按键输入 + window.focus(&self.focus_handle, cx); + + let rows = self.build_rows(); + let current_index = self.current_index(); + + // 全屏半透明遮罩:点击空白处关闭 + div() + .id("command-palette-backdrop") + .track_focus(&self.focus_handle) + .absolute() + .inset_0() + .bg(rgba(0x00000088)) + .flex() + .items_center() + .justify_center() + .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { + let key = event.keystroke.key.as_str(); + match key { + "escape" => { + cx.emit(CommandPaletteEvent::Close); + } + "up" | "arrowup" => this.move_selection(-1, cx), + "down" | "arrowdown" => this.move_selection(1, cx), + "enter" => { + if !this.filtered.is_empty() { + let idx = this.current_index(); + this.execute_at(idx, cx); + } + } + "backspace" => { + this.query.pop(); + this.selected_index = 0; + this.recompute_filtered(); + cx.notify(); + } + "space" => { + this.query.push(' '); + this.selected_index = 0; + this.recompute_filtered(); + cx.notify(); + } + _ => { + // 无修饰键时把可打印字符追加到查询串 + if !event.keystroke.modifiers.control + && !event.keystroke.modifiers.alt + && !event.keystroke.modifiers.platform + { + if let Some(ch) = &event.keystroke.key_char { + this.query.push_str(ch); + this.selected_index = 0; + this.recompute_filtered(); + cx.notify(); + } else if key.chars().count() == 1 { + this.query.push_str(key); + this.selected_index = 0; + this.recompute_filtered(); + cx.notify(); + } + } + } + } + })) + .on_mouse_down( + gpui_kit::MouseButton::Left, + cx.listener(|_this, _event, _window, cx| { + cx.emit(CommandPaletteEvent::Close); + }), + ) + .child( + v_flex() + .id("command-palette-card") + .w(px(704.0)) + .max_h(px(440.0)) + .bg(ThemeColors::surface()) + .border_1() + .border_color(ThemeColors::border()) + .rounded_lg() + .shadow_lg() + .overflow_hidden() + .on_mouse_down( + gpui_kit::MouseButton::Left, + cx.listener(|_this, _event, _window, cx| { + // 卡片内点击不冒泡到遮罩,避免误关闭 + cx.stop_propagation(); + }), + ) + .child( + // 1. 顶部输入行:搜索图标 + 查询串/占位 + Esc 徽标 + h_flex() + .h(px(52.0)) + .w_full() + .items_center() + .gap_2p5() + .px_4() + .border_b_1() + .border_color(ThemeColors::border()) + .child( + Icon::new(IconName::Search) + .size(px(16.0)) + .text_color(ThemeColors::primary()), + ) + .child( + h_flex() + .flex_1() + .items_center() + .gap_1() + .child( + div() + .text_sm() + .text_color(if self.query.is_empty() { + ThemeColors::subtle_foreground() + } else { + ThemeColors::foreground() + }) + .child(if self.query.is_empty() { + crate::i18n::menu_text( + cx, + "commandPalette.placeholder", + ) + .to_string() + } else { + self.query.clone() + }), + ) + .child( + // 静态光标条,提示可输入 + div().w(px(2.0)).h(px(16.0)).bg(ThemeColors::primary()), + ), + ) + .child(shortcut_badge("Esc")), + ) + .child( + // 2. 命令列表:按 category 分组,可滚动 + div() + .flex_1() + .w_full() + .max_h(px(340.0)) + .overflow_y_scrollbar() + .py_1() + .when(rows.is_empty(), |list| { + list.child( + div() + .w_full() + .py_8() + .text_center() + .text_sm() + .text_color(ThemeColors::subtle_foreground()) + .child(crate::i18n::menu_text(cx, "search.noResults")), + ) + }) + .children(rows.into_iter().map(|row| { + match row { + PaletteRow::Header(category) => h_flex() + .px_3() + .pt_2() + .pb_1() + .child( + div() + .text_xs() + .font_weight(FontWeight::MEDIUM) + .text_color(ThemeColors::muted_foreground()) + .child(palette_category_label(&category, cx)), + ) + .into_any_element(), + PaletteRow::Item(position) => { + let item = &self.items[self.filtered[position]]; + let is_selected = position == current_index; + let icon = item.icon; + let name = item.name.clone(); + let shortcut = item.shortcut.clone(); + + h_flex() + .id(("command-palette-item", position)) + .h(px(36.0)) + .w_full() + .mx_2() + .px_2p5() + .items_center() + .gap_2p5() + .rounded_md() + .cursor_pointer() + .when(is_selected, |row| { + row.bg(ThemeColors::selected()) + }) + .when(!is_selected, |row| { + row.hover(|h| h.bg(ThemeColors::accent())) + }) + .child(Icon::new(icon).size(px(15.0)).text_color( + if is_selected { + ThemeColors::primary() + } else { + ThemeColors::muted_foreground() + }, + )) + .child( + div() + .flex_1() + .text_sm() + .text_color(ThemeColors::foreground()) + .child(name), + ) + .when_some(shortcut, |row, sc| { + row.child(shortcut_badge(&sc)) + }) + .on_click(cx.listener( + move |this, _event, _window, cx| { + this.execute_at(position, cx); + }, + )) + .into_any_element() + } + } + })), + ), + ) + } +} + +/// 命令分组标题本地化:英文 category 映射到菜单键,未知回退原样。 +/// 条目构造处的 category 字段保持英文不动,只在渲染时映射。 +fn palette_category_label(category: &str, cx: &gpui_kit::App) -> String { + let key = match category { + "File" => Some("menu.file"), + "Edit" => Some("menu.edit"), + "View" => Some("menu.view"), + "Go" => Some("menu.go"), + "Terminal" => Some("menu.terminal"), + "Run" => Some("menu.run"), + "Tools" => Some("menu.tools"), + "Window" => Some("menu.window"), + "Help" => Some("menu.help"), + "Settings" => Some("menu.preferences"), + "Pane" => Some("menu.view"), + _ => None, + }; + match key { + Some(key) => crate::i18n::menu_text(cx, key).to_string(), + None => category.to_string(), + } +} + +/// 快捷键/键位徽标。 +fn shortcut_badge(label: &str) -> AnyElement { + div() + .px_1p5() + .py(px(1.0)) + .rounded_sm() + .bg(ThemeColors::accent()) + .border_1() + .border_color(ThemeColors::border()) + .text_xs() + .text_color(ThemeColors::muted_foreground()) + .child(label.to_string()) + .into_any_element() +} + +/// 构造一条命令,减少清单里的重复样板。 +fn command( + id: &str, + name: &str, + category: &str, + shortcut: Option<&str>, + icon: IconName, +) -> CommandPaletteItem { + CommandPaletteItem { + id: id.to_string(), + name: name.to_string(), + category: category.to_string(), + shortcut: shortcut.map(|s| s.to_string()), + icon, + } +} + +/// 与 Tauri 端动作工厂对齐的命令清单,按 category 顺序排列以支持列表分组。 +fn build_commands() -> Vec { + vec![ + // File + command( + "file.new", + "File: New File", + "File", + Some("Ctrl+N"), + IconName::Plus, + ), + command( + "file.open", + "File: Open Project...", + "File", + Some("Ctrl+O"), + IconName::FolderOpen, + ), + command( + "file.save", + "File: Save", + "File", + Some("Ctrl+S"), + IconName::Save, + ), + command( + "file.saveAll", + "File: Save All", + "File", + Some("Ctrl+Shift+S"), + IconName::SaveAll, + ), + command( + "file.closeEditor", + "File: Close Editor", + "File", + Some("Ctrl+W"), + IconName::Close, + ), + command( + "file.reopenClosed", + "File: Reopen Closed Editor", + "File", + Some("Ctrl+Shift+T"), + IconName::RotateCw, + ), + // Edit + command( + "edit.undo", + "Edit: Undo", + "Edit", + Some("Ctrl+Z"), + IconName::Undo, + ), + command( + "edit.redo", + "Edit: Redo", + "Edit", + Some("Ctrl+Shift+Z"), + IconName::Redo, + ), + command( + "edit.cut", + "Edit: Cut", + "Edit", + Some("Ctrl+X"), + IconName::Scissors, + ), + command( + "edit.copy", + "Edit: Copy", + "Edit", + Some("Ctrl+C"), + IconName::Copy, + ), + command( + "edit.paste", + "Edit: Paste", + "Edit", + Some("Ctrl+V"), + IconName::ClipboardPaste, + ), + command( + "edit.find", + "Edit: Find", + "Edit", + Some("Ctrl+F"), + IconName::Search, + ), + command( + "edit.replace", + "Edit: Replace", + "Edit", + Some("Ctrl+H"), + IconName::RefreshCw, + ), + command( + "edit.format", + "Edit: Format Document", + "Edit", + Some("Shift+Alt+F"), + IconName::Code, + ), + // View + command( + "workbench.toggle_sidebar", + "View: Toggle Sidebar", + "View", + Some("Ctrl+B"), + IconName::PanelLeft, + ), + command( + "workbench.toggle_terminal", + "View: Toggle Bottom Panel", + "View", + Some("Ctrl+J"), + IconName::PanelBottom, + ), + command( + "view.zoomIn", + "View: Zoom In", + "View", + Some("Ctrl+="), + IconName::Plus, + ), + command( + "view.zoomOut", + "View: Zoom Out", + "View", + Some("Ctrl+-"), + IconName::Minus, + ), + command( + "view.resetZoom", + "View: Reset Zoom", + "View", + Some("Ctrl+0"), + IconName::RotateCw, + ), + command( + "view.colorTheme", + "View: Color Theme", + "View", + None, + IconName::Palette, + ), + command( + "view.iconTheme", + "View: File Icon Theme", + "View", + None, + IconName::Box, + ), + // Go + command( + "go.quickOpen", + "Go: Quick Open", + "Go", + Some("Ctrl+P"), + IconName::Search, + ), + command( + "go.commandPalette", + "Go: Command Palette", + "Go", + Some("Ctrl+Shift+P"), + IconName::Search, + ), + command( + "go.symbolInFile", + "Go: Symbol in File", + "Go", + Some("Ctrl+Shift+O"), + IconName::Code, + ), + command( + "go.workspaceSymbol", + "Go: Symbol in Workspace", + "Go", + Some("Ctrl+T"), + IconName::Code, + ), + command( + "go.goToLine", + "Go: Go to Line", + "Go", + Some("Ctrl+G"), + IconName::Hash, + ), + command( + "go.back", + "Go: Back", + "Go", + Some("Alt+Left"), + IconName::ArrowLeft, + ), + command( + "go.forward", + "Go: Forward", + "Go", + Some("Alt+Right"), + IconName::ArrowRight, + ), + command( + "go.nextEditor", + "Go: Next Editor", + "Go", + Some("Ctrl+Tab"), + IconName::ArrowRight, + ), + // Terminal + command( + "terminal.new", + "Terminal: New Terminal", + "Terminal", + Some("Ctrl+`"), + IconName::Terminal, + ), + command( + "terminal.split", + "Terminal: Split Terminal", + "Terminal", + None, + IconName::Columns2, + ), + command( + "terminal.clear", + "Terminal: Clear", + "Terminal", + None, + IconName::Trash, + ), + command( + "terminal.toggle", + "Terminal: Toggle Panel", + "Terminal", + Some("Ctrl+J"), + IconName::PanelBottom, + ), + // Run + command( + "run.start", + "Run: Start", + "Run", + Some("Shift+F10"), + IconName::Play, + ), + command( + "run.debug", + "Run: Start Debugging", + "Run", + Some("F5"), + IconName::Bug, + ), + command( + "run.stop", + "Run: Stop", + "Run", + Some("Shift+F5"), + IconName::Square, + ), + command( + "run.withoutDebug", + "Run: Run Without Debugging", + "Run", + Some("Ctrl+F5"), + IconName::Play, + ), + command( + "run.build", + "Run: Build Project", + "Run", + Some("Ctrl+Shift+B"), + IconName::Box, + ), + command( + "run.tests", + "Run: Run Tests", + "Run", + None, + IconName::CircleCheck, + ), + // Tools + command( + "tools.keyboardShortcuts", + "Tools: Keyboard Shortcuts", + "Tools", + Some("Ctrl+K Ctrl+S"), + IconName::Keyboard, + ), + command( + "tools.developerTools", + "Tools: Toggle Developer Tools", + "Tools", + Some("Ctrl+Shift+I"), + IconName::Bug, + ), + command( + "tools.showLogs", + "Tools: Show Logs", + "Tools", + None, + IconName::Terminal, + ), + command( + "tools.database", + "Tools: Open Database", + "Tools", + None, + IconName::Database, + ), + command( + "tools.memory", + "Tools: Memory Usage", + "Tools", + None, + IconName::MemoryStick, + ), + command( + "tools.languageServers", + "Tools: Language Servers", + "Tools", + None, + IconName::Info, + ), + // Window + command( + "window.minimize", + "Window: Minimize", + "Window", + None, + IconName::Minimize, + ), + command( + "window.maximize", + "Window: Maximize", + "Window", + None, + IconName::Maximize, + ), + command( + "window.toggleFullscreen", + "Window: Toggle Fullscreen", + "Window", + Some("F11"), + IconName::Maximize, + ), + command( + "window.reload", + "Window: Reload", + "Window", + Some("Ctrl+R"), + IconName::RotateCw, + ), + command( + "window.close", + "Window: Close", + "Window", + Some("Ctrl+Shift+W"), + IconName::Close, + ), + // Help + command( + "help.documentation", + "Help: Documentation", + "Help", + None, + IconName::BookOpen, + ), + command( + "help.reportIssue", + "Help: Report Issue", + "Help", + None, + IconName::ExternalLink, + ), + command( + "help.checkForUpdates", + "Help: Check for Updates", + "Help", + None, + IconName::RotateCw, + ), + command( + "help.about", + "Help: About Lithe", + "Help", + None, + IconName::Info, + ), + command("help.welcome", "Help: Welcome", "Help", None, IconName::Zap), + // Settings + command( + "workbench.open_settings", + "Settings: Open Settings", + "Settings", + Some("Ctrl+,"), + IconName::Settings, + ), + command( + "workbench.open_settings_appearance", + "Settings: Appearance", + "Settings", + None, + IconName::SlidersHorizontal, + ), + command( + "workbench.open_settings_keybindings", + "Settings: Keybindings", + "Settings", + None, + IconName::Keyboard, + ), + command( + "workbench.open_settings_theme", + "Settings: Color Theme", + "Settings", + None, + IconName::Palette, + ), + // Pane + command( + "view.split_editor_right", + "Pane: Split Editor Right", + "Pane", + None, + IconName::Columns2, + ), + command( + "view.split_editor_down", + "Pane: Split Editor Down", + "Pane", + None, + IconName::Rows2, + ), + command( + "view.close_editor_group", + "Pane: Close Editor Group", + "Pane", + None, + IconName::Close, + ), + command( + "view.close_other_groups", + "Pane: Close Other Editor Groups", + "Pane", + None, + IconName::Close, + ), + command( + "view.move_editor_next_group", + "Pane: Move Editor to Next Group", + "Pane", + None, + IconName::ArrowRight, + ), + command( + "view.move_editor_previous_group", + "Pane: Move Editor to Previous Group", + "Pane", + None, + IconName::ArrowLeft, + ), + command( + "view.reset_editor_group_sizes", + "Pane: Reset Editor Group Sizes", + "Pane", + None, + IconName::Columns2, + ), + command( + "view.toggle_editor_group_lock", + "Pane: Toggle Editor Group Lock", + "Pane", + None, + IconName::Lock, + ), + ] +} diff --git a/linux/src/workbench/editor.rs b/linux/src/workbench/editor.rs new file mode 100644 index 000000000..d40a745b4 --- /dev/null +++ b/linux/src/workbench/editor.rs @@ -0,0 +1,1383 @@ +use gpui_kit::assets::IconName; +use gpui_kit::component::input::{Editor, EditorState, InputEvent, Position}; +use gpui_kit::component::menu::ContextMenuExt as _; +use gpui_kit::component::scroll::ScrollableElement as _; +use gpui_kit::component::{h_flex, v_flex, Icon}; +use gpui_kit::prelude::FluentBuilder as _; +use gpui_kit::{ + div, px, relative, AppContext as _, ClipboardItem, Context, Entity, EventEmitter, FontWeight, + InteractiveElement as _, IntoElement, ParentElement as _, Render, + StatefulInteractiveElement as _, Styled as _, Subscription, Window, +}; + +use std::rc::Rc; + +use super::panes::PaneId; +use super::tab_menu::{SplitCallback, ToggleLockCallback}; + +use crate::core::CoreClient; +use crate::settings; +use crate::theme::ThemeColors; + +#[derive(Debug, Clone)] +pub struct EditorTab { + pub path: String, + pub title: String, + pub content: String, + pub is_dirty: bool, + /// 固定标签:批量关闭跳过,固定组永远排在标签栏前段。 + pub is_pinned: bool, + #[allow(dead_code)] + pub cursor_line: usize, + #[allow(dead_code)] + pub cursor_col: usize, +} + +/// 多标签代码编辑器组件(对齐 macOS LitheTheme / IntelliJ 视觉规范) +/// +/// 正文复用上游 `gpui_kit::component::input::Editor`,由它负责滚动、 +/// 文本选择与语法高亮;本组件只管理标签栏、面包屑与文件读写。 +pub struct EditorView { + pub workspace_root: String, + pub tabs: Vec, + pub active_tab_index: Option, + #[allow(dead_code)] + pub encoding: String, + /// 当前活动标签对应的上游编辑器状态,随视图生命周期常驻并复用。 + editor_state: Entity, + /// 自动换行开关镜像(上游无 getter,本地持有后取反下发)。 + soft_wrap: bool, + /// 行号显示开关镜像。 + show_line_numbers: bool, + /// 空白字符显示开关镜像。 + show_whitespace: bool, + /// 已同步到 `editor_state` 的标签索引。 + synced_tab: Option, + /// 活动标签内容被外部替换(重新打开文件)时置位,渲染时重新灌入编辑器。 + sync_needed: bool, + /// 监听编辑器文本变化以回写标签内容与脏标记。 + _editor_subscription: Subscription, + client: CoreClient, + /// 撤销/重做历史(全文快照,栈顶恒等于编辑器当前值)。 + undo_stack: Vec, + redo_stack: Vec, + /// 已关闭标签(标签快照,原索引),后进先出供恢复。 + closed_stack: Vec<(EditorTab, usize)>, + /// 右键菜单目标标签(UI 层读写,方法层不消费)。 + pub context_menu_tab: Option, + /// 所属窗格(工作台装配;`None` 时右键菜单用 0 与空回调兜底)。 + pub pane_id: Option, + /// 所属窗格锁定镜像(工作台回写,决定锁定项文案)。 + pub pane_locked: bool, + /// 拆分回调(工作台注入,经 workbench entity 回写)。 + pub on_split: Option, + /// 锁定回调(工作台注入,经 workbench entity 回写)。 + pub on_toggle_lock: Option, +} + +impl EditorView { + pub fn new(workspace_root: String, window: &mut Window, cx: &mut Context) -> Self { + let soft_wrap = settings::get(cx).word_wrap; + let show_line_numbers = settings::get(cx).line_numbers; + let editor_state = cx.new(|cx| { + EditorState::new(window, cx) + .line_number(show_line_numbers) + .soft_wrap(soft_wrap) + }); + let _editor_subscription = cx.subscribe_in( + &editor_state, + window, + |this, _state, event: &InputEvent, _window, cx| { + if matches!(event, InputEvent::Change) { + this.on_editor_change(cx); + } + }, + ); + + Self { + workspace_root, + tabs: Vec::new(), + active_tab_index: None, + encoding: "UTF-8".to_string(), + editor_state, + soft_wrap, + show_line_numbers, + show_whitespace: false, + synced_tab: None, + sync_needed: false, + _editor_subscription, + client: CoreClient::new(), + undo_stack: Vec::new(), + redo_stack: Vec::new(), + closed_stack: Vec::new(), + context_menu_tab: None, + pane_id: None, + pane_locked: false, + on_split: None, + on_toggle_lock: None, + } + } + + /// 打开文件,如果已在标签中则切换,否则新增标签 + pub fn open_file(&mut self, path: String, content: String, cx: &mut Context) { + if let Some(pos) = self.tabs.iter().position(|t| t.path == path) { + if let Some(tab) = self.tabs.get_mut(pos) { + if tab.content != content { + tab.content = content; + tab.is_dirty = false; + self.sync_needed = true; + } + } + self.active_tab_index = Some(pos); + self.publish_active_document(cx); + cx.notify(); + return; + } + + let title = path + .rsplit_once('/') + .map(|(_, name)| name.to_string()) + .unwrap_or_else(|| path.clone()); + + self.tabs.push(EditorTab { + path, + title, + content, + is_dirty: false, + is_pinned: false, + cursor_line: 1, + cursor_col: 1, + }); + + self.active_tab_index = Some(self.tabs.len() - 1); + self.sync_needed = true; + self.publish_active_document(cx); + cx.notify(); + } + + /// 关闭指定索引的标签页 + pub fn close_tab(&mut self, index: usize, cx: &mut Context) { + if index < self.tabs.len() { + self.record_closed(index); + self.tabs.remove(index); + if self.tabs.is_empty() { + self.active_tab_index = None; + } else if let Some(current) = self.active_tab_index { + if current >= self.tabs.len() { + self.active_tab_index = Some(self.tabs.len() - 1); + } else if current > index { + self.active_tab_index = Some(current - 1); + } + } + self.sync_needed = true; + cx.notify(); + } + } + + /// 把当前活动标签的文本与语言灌入上游编辑器。 + fn sync_active_editor(&mut self, window: &mut Window, cx: &mut Context) { + let active_index = self.active_tab_index; + + if self.synced_tab == active_index && !self.sync_needed { + return; + } + + let Some(index) = active_index else { + self.synced_tab = None; + self.sync_needed = false; + return; + }; + + let Some(tab) = self.tabs.get(index).cloned() else { + return; + }; + + let language = Self::language_name(&tab.path).to_lowercase(); + // 上游 `Language::from_name` 只认 `bash`/`sh`,`language_name` 的 `Shell` + // 转小写后是 `shell`,此处映射回 `sh` 否则高亮回退到 Plain。 + let language = if language == "shell" { + "sh" + } else { + language.as_str() + }; + let content = tab.content.clone(); + self.editor_state.update(cx, |editor, cx| { + editor.set_value(content, window, cx); + editor.set_highlighter(language, cx); + }); + + self.synced_tab = Some(index); + self.sync_needed = false; + self.publish_active_document(cx); + } + + /// 向父视图广播当前活动文档(无活动标签时不广播)。 + fn publish_active_document(&mut self, cx: &mut Context) { + let Some(tab) = self.active_tab_index.and_then(|index| self.tabs.get(index)) else { + return; + }; + cx.emit(EditorTabEvent::DocumentChanged { + path: tab.path.clone(), + text: tab.content.clone(), + }); + } + + /// 编辑器文本变化后回写标签内容并标记为已修改。 + fn on_editor_change(&mut self, cx: &mut Context) { + let Some(index) = self.active_tab_index else { + return; + }; + + let value = self.editor_state.read(cx).value().to_string(); + if let Some(tab) = self.tabs.get_mut(index) { + if tab.content != value { + tab.content = value.clone(); + tab.is_dirty = true; + } + } + if self.undo_stack.last().is_none_or(|top| *top != value) { + self.undo_stack.push(value); + if self.undo_stack.len() > 100 { + let overflow = self.undo_stack.len() - 100; + self.undo_stack.drain(..overflow); + } + self.redo_stack.clear(); + } + self.publish_active_document(cx); + cx.notify(); + } + + /// 返回当前活动文件路径,供 Run 构造 Current File 请求。 + pub fn active_file_path(&self) -> Option { + let index = self.active_tab_index?; + self.tabs.get(index).map(|tab| tab.path.clone()) + } + + /// 保存当前活动的标签页至磁盘(对接 `file.write`)。 + pub fn save_active(&mut self, cx: &mut Context) { + self.save_active_task(cx).detach(); + } + + /// Run 前等待保存完成;错误原样返回给启动工作流。 + pub fn save_active_task( + &mut self, + cx: &mut Context, + ) -> gpui_kit::Task> { + let Some(idx) = self.active_tab_index else { + return cx.spawn(async move |_this, _cx| Ok(())); + }; + let Some(tab) = self.tabs.get(idx) else { + return cx.spawn(async move |_this, _cx| Ok(())); + }; + if !tab.is_dirty { + return cx.spawn(async move |_this, _cx| Ok(())); + } + + // 以编辑器内的实时文本为准,避免依赖事件回写的时序。 + let text = self.editor_state.read(cx).value().to_string(); + let root = self.workspace_root.clone(); + let path = tab.path.clone(); + let client = self.client.clone(); + cx.spawn(async move |this, cx| { + client.write_file(&cx, &root, &path, &text).await?; + let _ = this.update(cx, |ed, cx| { + if let Some(active_idx) = ed.active_tab_index { + if let Some(tab) = ed.tabs.get_mut(active_idx) { + if tab.path == path { + tab.is_dirty = false; + cx.notify(); + } + } + } + }); + Ok(()) + }) + } + + /// 全文替换并记入撤销历史(`set_value` 不触发 Change,需手动维护历史与标签同步)。 + fn commit_full_text(&mut self, new_text: String, window: &mut Window, cx: &mut Context) { + let current = self.editor_state.read(cx).value().to_string(); + if current == new_text { + return; + } + self.undo_stack.push(current); + self.undo_stack.push(new_text.clone()); + if self.undo_stack.len() > 100 { + let overflow = self.undo_stack.len() - 100; + self.undo_stack.drain(..overflow); + } + self.redo_stack.clear(); + self.editor_state.update(cx, |editor, cx| { + editor.set_value(new_text.clone(), window, cx); + }); + if let Some(index) = self.active_tab_index { + if let Some(tab) = self.tabs.get_mut(index) { + tab.content = new_text; + tab.is_dirty = true; + } + } + cx.notify(); + } + + /// 撤销到上一个历史快照(空栈或无更早状态直接返回)。 + pub fn undo(&mut self, window: &mut Window, cx: &mut Context) { + let current = self.editor_state.read(cx).value().to_string(); + if self.undo_stack.last().is_none_or(|top| *top != current) { + return; + } + self.undo_stack.pop(); + let Some(prev) = self.undo_stack.last().cloned() else { + self.undo_stack.push(current); + return; + }; + self.redo_stack.push(current); + if self.redo_stack.len() > 100 { + self.redo_stack.remove(0); + } + self.editor_state.update(cx, |editor, cx| { + editor.set_value(prev.clone(), window, cx); + }); + if let Some(index) = self.active_tab_index { + if let Some(tab) = self.tabs.get_mut(index) { + tab.content = prev; + tab.is_dirty = true; + } + } + cx.notify(); + } + + /// 重做上一次撤销(空栈直接返回)。 + pub fn redo(&mut self, window: &mut Window, cx: &mut Context) { + let Some(next) = self.redo_stack.pop() else { + return; + }; + let current = self.editor_state.read(cx).value().to_string(); + if current == next { + self.redo_stack.push(next); + return; + } + self.undo_stack.push(next.clone()); + if self.undo_stack.len() > 100 { + let overflow = self.undo_stack.len() - 100; + self.undo_stack.drain(..overflow); + } + self.editor_state.update(cx, |editor, cx| { + editor.set_value(next.clone(), window, cx); + }); + if let Some(index) = self.active_tab_index { + if let Some(tab) = self.tabs.get_mut(index) { + tab.content = next; + tab.is_dirty = true; + } + } + cx.notify(); + } + + /// 全选当前编辑器文本。 + pub fn select_all(&mut self, window: &mut Window, cx: &mut Context) { + if self.active_tab_index.is_none() { + return; + } + self.editor_state.update(cx, |editor, cx| { + editor.select_all(window, cx); + }); + } + + /// 复制选区到剪贴板(空选区直接返回)。 + pub fn copy(&mut self, cx: &mut Context) { + if self.active_tab_index.is_none() { + return; + } + let text = self.editor_state.read(cx).selected_value().to_string(); + if text.is_empty() { + return; + } + cx.write_to_clipboard(ClipboardItem::new_string(text)); + } + + /// 剪切选区到剪贴板(空选区直接返回)。 + pub fn cut(&mut self, window: &mut Window, cx: &mut Context) { + if self.active_tab_index.is_none() { + return; + } + let text = self.editor_state.read(cx).selected_value().to_string(); + if text.is_empty() { + return; + } + cx.write_to_clipboard(ClipboardItem::new_string(text)); + self.editor_state.update(cx, |editor, cx| { + editor.replace("", window, cx); + }); + } + + /// 粘贴剪贴板文本(无内容直接返回;有选区则替换,否则在光标处插入)。 + pub fn paste(&mut self, window: &mut Window, cx: &mut Context) { + if self.active_tab_index.is_none() { + return; + } + let Some(item) = cx.read_from_clipboard() else { + return; + }; + let text = item.text().unwrap_or_default().to_string(); + if text.is_empty() { + return; + } + let range = self.editor_state.read(cx).selected_range(); + if range.start != range.end { + self.editor_state.update(cx, |editor, cx| { + editor.replace(text, window, cx); + }); + } else { + self.editor_state.update(cx, |editor, cx| { + editor.insert(text, window, cx); + }); + } + } + + /// 当前光标行(0-based)与编辑器全文。 + fn active_row(&mut self, cx: &mut Context) -> Option<(usize, String)> { + if self.active_tab_index.is_none() { + return None; + } + let state = self.editor_state.read(cx); + let row = state.cursor_position().line as usize; + let value = state.value().to_string(); + Some((row, value)) + } + + /// 全文替换后把光标移到指定行首。 + fn move_cursor_to_row(&mut self, row: usize, window: &mut Window, cx: &mut Context) { + self.editor_state.update(cx, |editor, cx| { + editor.set_cursor_position(Position::new(row as u32, 0), window, cx); + }); + } + + /// 复制当前行到下一行。 + pub fn duplicate_line(&mut self, window: &mut Window, cx: &mut Context) { + let Some((row, value)) = self.active_row(cx) else { + return; + }; + let mut lines: Vec = value.split('\n').map(str::to_string).collect(); + if lines.is_empty() { + return; + } + let row = row.min(lines.len() - 1); + lines.insert(row + 1, lines[row].clone()); + self.commit_full_text(lines.join("\n"), window, cx); + self.move_cursor_to_row(row + 1, window, cx); + } + + /// 删除当前行(含换行,末行特殊处理)。 + pub fn delete_line(&mut self, window: &mut Window, cx: &mut Context) { + let Some((row, value)) = self.active_row(cx) else { + return; + }; + let mut lines: Vec = value.split('\n').map(str::to_string).collect(); + if lines.is_empty() { + return; + } + let row = row.min(lines.len() - 1); + lines.remove(row); + self.commit_full_text(lines.join("\n"), window, cx); + if !lines.is_empty() { + self.move_cursor_to_row(row.min(lines.len() - 1), window, cx); + } + } + + /// 当前行与上一行交换(首行直接返回)。 + pub fn move_line_up(&mut self, window: &mut Window, cx: &mut Context) { + let Some((row, value)) = self.active_row(cx) else { + return; + }; + let mut lines: Vec = value.split('\n').map(str::to_string).collect(); + if lines.is_empty() { + return; + } + let row = row.min(lines.len() - 1); + if row == 0 { + return; + } + lines.swap(row, row - 1); + self.commit_full_text(lines.join("\n"), window, cx); + self.move_cursor_to_row(row - 1, window, cx); + } + + /// 当前行与下一行交换(末行直接返回)。 + pub fn move_line_down(&mut self, window: &mut Window, cx: &mut Context) { + let Some((row, value)) = self.active_row(cx) else { + return; + }; + let mut lines: Vec = value.split('\n').map(str::to_string).collect(); + if lines.is_empty() { + return; + } + let row = row.min(lines.len() - 1); + if row + 1 >= lines.len() { + return; + } + lines.swap(row, row + 1); + self.commit_full_text(lines.join("\n"), window, cx); + self.move_cursor_to_row(row + 1, window, cx); + } + + /// 切换当前行注释(已注释则去注释,保留缩进)。 + pub fn toggle_comment(&mut self, window: &mut Window, cx: &mut Context) { + let Some(index) = self.active_tab_index else { + return; + }; + let path = self + .tabs + .get(index) + .map(|tab| tab.path.clone()) + .unwrap_or_default(); + let Some((row, value)) = self.active_row(cx) else { + return; + }; + let mut lines: Vec = value.split('\n').map(str::to_string).collect(); + if lines.is_empty() { + return; + } + let row = row.min(lines.len() - 1); + let line = lines[row].clone(); + let indent_len = line.len() - line.trim_start().len(); + let (indent, body) = line.split_at(indent_len); + let new_line = match Self::comment_style(&path) { + CommentStyle::Line(prefix) => { + if let Some(rest) = body.strip_prefix(prefix) { + let rest = rest + .strip_prefix(' ') + .or_else(|| rest.strip_prefix('\t')) + .unwrap_or(rest); + format!("{indent}{rest}") + } else { + format!("{indent}{prefix} {body}") + } + } + CommentStyle::Block => { + let trimmed = line.trim(); + if let Some(inner) = trimmed + .strip_prefix("")) + { + format!("{indent}{}", inner.trim()) + } else { + format!("{indent}") + } + } + }; + lines[row] = new_line; + self.commit_full_text(lines.join("\n"), window, cx); + self.move_cursor_to_row(row, window, cx); + } + + /// 记录关闭的标签以供恢复(上限 30,超限去头;脏标签直接关,与 `close_tab` 一致)。 + fn record_closed(&mut self, index: usize) { + if let Some(tab) = self.tabs.get(index).cloned() { + self.closed_stack.push((tab, index)); + if self.closed_stack.len() > 30 { + let overflow = self.closed_stack.len() - 30; + self.closed_stack.drain(..overflow); + } + } + } + + /// 关闭全部标签(逐个进恢复栈;固定标签保留)。 + pub fn close_all_tabs(&mut self, cx: &mut Context) { + if self.tabs.is_empty() { + return; + } + let active_path = self + .active_tab_index + .and_then(|index| self.tabs.get(index)) + .map(|tab| tab.path.clone()); + let mut kept = Vec::new(); + for (index, tab) in self.tabs.drain(..).enumerate() { + if tab.is_pinned { + kept.push(tab); + } else { + self.closed_stack.push((tab, index)); + } + } + if self.closed_stack.len() > 30 { + let overflow = self.closed_stack.len() - 30; + self.closed_stack.drain(..overflow); + } + self.tabs = kept; + self.active_tab_index = + active_path.and_then(|path| self.tabs.iter().position(|tab| tab.path == path)); + self.sync_needed = true; + cx.notify(); + } + + /// 关闭除活动标签外的全部标签(固定标签保留,固定组在前)。 + pub fn close_other_tabs(&mut self, cx: &mut Context) { + let Some(active) = self.active_tab_index else { + return; + }; + if active >= self.tabs.len() { + return; + } + let active_path = self.tabs.get(active).map(|tab| tab.path.clone()); + let mut kept: Vec<(usize, EditorTab)> = Vec::new(); + for (index, tab) in self.tabs.drain(..).enumerate() { + if index == active || tab.is_pinned { + kept.push((index, tab)); + } else { + self.closed_stack.push((tab, index)); + } + } + if self.closed_stack.len() > 30 { + let overflow = self.closed_stack.len() - 30; + self.closed_stack.drain(..overflow); + } + // 固定组在前并保持原相对顺序。 + kept.sort_by_key(|(index, tab)| (!tab.is_pinned, *index)); + self.tabs = kept.into_iter().map(|(_, tab)| tab).collect(); + self.active_tab_index = + active_path.and_then(|path| self.tabs.iter().position(|tab| tab.path == path)); + self.sync_needed = true; + cx.notify(); + } + + /// 关闭全部未修改(`is_dirty == false`)的标签(固定标签保留)。 + pub fn close_saved_tabs(&mut self, cx: &mut Context) { + if self.tabs.is_empty() { + return; + } + let active_path = self + .active_tab_index + .and_then(|index| self.tabs.get(index)) + .map(|tab| tab.path.clone()); + let mut kept = Vec::new(); + for (index, tab) in self.tabs.drain(..).enumerate() { + if tab.is_dirty || tab.is_pinned { + kept.push(tab); + } else { + self.closed_stack.push((tab, index)); + } + } + if self.closed_stack.len() > 30 { + let overflow = self.closed_stack.len() - 30; + self.closed_stack.drain(..overflow); + } + self.tabs = kept; + self.active_tab_index = + active_path.and_then(|path| self.tabs.iter().position(|tab| tab.path == path)); + self.sync_needed = true; + cx.notify(); + } + + /// 关闭活动标签左侧的全部标签(固定标签保留在前段)。 + pub fn close_tabs_to_left(&mut self, cx: &mut Context) { + let Some(active) = self.active_tab_index else { + return; + }; + if active == 0 || active >= self.tabs.len() { + return; + } + let active_path = self.tabs.get(active).map(|tab| tab.path.clone()); + let mut pinned_left: Vec = Vec::new(); + for (index, tab) in self.tabs.drain(..active).enumerate() { + if tab.is_pinned { + pinned_left.push(tab); + } else { + self.closed_stack.push((tab, index)); + } + } + if self.closed_stack.len() > 30 { + let overflow = self.closed_stack.len() - 30; + self.closed_stack.drain(..overflow); + } + for (offset, tab) in pinned_left.into_iter().enumerate() { + self.tabs.insert(offset, tab); + } + self.active_tab_index = + active_path.and_then(|path| self.tabs.iter().position(|tab| tab.path == path)); + self.sync_needed = true; + cx.notify(); + } + + /// 关闭活动标签右侧的全部标签(固定标签保留)。 + pub fn close_tabs_to_right(&mut self, cx: &mut Context) { + let Some(active) = self.active_tab_index else { + return; + }; + if active >= self.tabs.len() || active + 1 >= self.tabs.len() { + return; + } + let mut pinned_right: Vec = Vec::new(); + for (offset, tab) in self.tabs.drain(active + 1..).enumerate() { + if tab.is_pinned { + pinned_right.push(tab); + } else { + self.closed_stack.push((tab, active + 1 + offset)); + } + } + if self.closed_stack.len() > 30 { + let overflow = self.closed_stack.len() - 30; + self.closed_stack.drain(..overflow); + } + self.tabs.extend(pinned_right); + self.sync_needed = true; + cx.notify(); + } + + /// 关闭除 `idx` 外的全部标签(`close_other_tabs` 的指定索引版,供右键菜单调用;固定标签保留,固定组在前)。 + pub fn close_others_at(&mut self, idx: usize, cx: &mut Context) { + if idx >= self.tabs.len() { + return; + } + let kept_path = self.tabs.get(idx).map(|tab| tab.path.clone()); + let mut kept: Vec<(usize, EditorTab)> = Vec::new(); + for (index, tab) in self.tabs.drain(..).enumerate() { + if index == idx || tab.is_pinned { + kept.push((index, tab)); + } else { + self.closed_stack.push((tab, index)); + } + } + if self.closed_stack.len() > 30 { + let overflow = self.closed_stack.len() - 30; + self.closed_stack.drain(..overflow); + } + // 固定组在前并保持原相对顺序;`idx` 未固定时排在固定段之后。 + kept.sort_by_key(|(index, tab)| (!tab.is_pinned, *index)); + self.tabs = kept.into_iter().map(|(_, tab)| tab).collect(); + self.active_tab_index = + kept_path.and_then(|path| self.tabs.iter().position(|tab| tab.path == path)); + self.sync_needed = true; + cx.notify(); + } + + /// 关闭 `idx` 右侧的全部标签(`close_tabs_to_right` 的指定索引版,供右键菜单调用;固定标签保留)。 + pub fn close_to_right_at(&mut self, idx: usize, cx: &mut Context) { + if idx >= self.tabs.len() || idx + 1 >= self.tabs.len() { + return; + } + let active_path = self + .active_tab_index + .and_then(|index| self.tabs.get(index)) + .map(|tab| tab.path.clone()); + let mut pinned_right: Vec = Vec::new(); + for (offset, tab) in self.tabs.drain(idx + 1..).enumerate() { + if tab.is_pinned { + pinned_right.push(tab); + } else { + self.closed_stack.push((tab, idx + 1 + offset)); + } + } + if self.closed_stack.len() > 30 { + let overflow = self.closed_stack.len() - 30; + self.closed_stack.drain(..overflow); + } + self.tabs.extend(pinned_right); + if let Some(active) = self.active_tab_index { + if active > idx { + // 活动页被关掉时回退到 `idx`;固定活动页幸存时按 path 跟随。 + self.active_tab_index = active_path + .and_then(|path| self.tabs.iter().position(|tab| tab.path == path)) + .or(Some(idx)); + } + } + self.sync_needed = true; + cx.notify(); + } + + /// 翻转 `idx` 标签的固定状态并物理重排(固定组永远在前)。 + /// 固定→移到固定段末尾,取消固定→移到固定段之后紧邻;活动页与已同步 + /// 位按 path 跟随被移动的标签。内容未变故不置 `sync_needed`,只 `notify`。 + pub fn toggle_pin_at(&mut self, idx: usize, cx: &mut Context) { + if idx >= self.tabs.len() { + return; + } + let active_path = self + .active_tab_index + .and_then(|index| self.tabs.get(index)) + .map(|tab| tab.path.clone()); + let synced_path = self + .synced_tab + .and_then(|index| self.tabs.get(index)) + .map(|tab| tab.path.clone()); + let mut tab = self.tabs.remove(idx); + tab.is_pinned = !tab.is_pinned; + let pinned_count = self.tabs.iter().filter(|tab| tab.is_pinned).count(); + self.tabs.insert(pinned_count.min(self.tabs.len()), tab); + if let Some(path) = active_path { + self.active_tab_index = self.tabs.iter().position(|tab| tab.path == path); + } + if let Some(path) = synced_path { + self.synced_tab = self.tabs.iter().position(|tab| tab.path == path); + } + cx.notify(); + } + + /// 从磁盘重读指定标签(失败忽略;同步走既有 `sync_needed` 机制)。 + pub fn reload_tab(&mut self, idx: usize, cx: &mut Context) { + let Some(tab) = self.tabs.get(idx) else { + return; + }; + let path = tab.path.clone(); + let root = self.workspace_root.clone(); + let client = self.client.clone(); + + cx.spawn(async move |this, cx| { + let task = client.read_file(&cx, &root, &path); + if let Ok(text) = task.await { + let _ = this.update(cx, |ed, cx| { + if let Some(t) = ed.tabs.get_mut(idx) { + if t.path == path { + t.content = text; + t.is_dirty = false; + ed.sync_needed = true; + cx.notify(); + } + } + }); + } + }) + .detach(); + } + + /// 返回指定标签的绝对路径(`workspace_root/path` 拼接,供复制路径/文件管理器显示用)。 + pub fn tab_abs_path(&self, idx: usize) -> Option { + self.tabs + .get(idx) + .map(|tab| format!("{}/{}", self.workspace_root.trim_end_matches('/'), tab.path)) + } + + /// 恢复最近关闭的标签到原位(后进先出,活动标签指向恢复项)。 + pub fn reopen_closed_tab(&mut self, cx: &mut Context) { + let Some((tab, index)) = self.closed_stack.pop() else { + return; + }; + if let Some(pos) = self.tabs.iter().position(|t| t.path == tab.path) { + self.active_tab_index = Some(pos); + } else { + let insert_at = index.min(self.tabs.len()); + self.tabs.insert(insert_at, tab); + self.active_tab_index = Some(insert_at); + } + self.sync_needed = true; + cx.notify(); + } + + /// 保存全部脏标签至磁盘(仿 `save_active` 的落盘写法)。 + pub fn save_all_tabs(&mut self, cx: &mut Context) { + if self.tabs.is_empty() { + return; + } + // 以编辑器内的实时文本为准,避免依赖事件回写的时序。 + let live = self.editor_state.read(cx).value().to_string(); + let mut jobs: Vec<(String, String)> = Vec::new(); + for (index, tab) in self.tabs.iter().enumerate() { + if !tab.is_dirty { + continue; + } + let text = if Some(index) == self.active_tab_index { + live.clone() + } else { + tab.content.clone() + }; + jobs.push((tab.path.clone(), text)); + } + if jobs.is_empty() { + return; + } + let root = self.workspace_root.clone(); + let client = self.client.clone(); + cx.spawn(async move |this, cx| { + for (path, text) in jobs { + if client.write_file(&cx, &root, &path, &text).await.is_ok() { + let _ = this.update(cx, |ed, cx| { + if let Some(tab) = ed.tabs.iter_mut().find(|tab| tab.path == path) { + tab.is_dirty = false; + } + cx.notify(); + }); + } + } + }) + .detach(); + } + + /// 切换到下一个标签(循环)。 + pub fn goto_next_tab(&mut self, cx: &mut Context) { + if self.tabs.is_empty() { + return; + } + let next = match self.active_tab_index { + Some(index) => (index + 1) % self.tabs.len(), + None => 0, + }; + self.active_tab_index = Some(next); + self.sync_needed = true; + cx.notify(); + } + + /// 切换到上一个标签(循环)。 + pub fn goto_prev_tab(&mut self, cx: &mut Context) { + if self.tabs.is_empty() { + return; + } + let prev = match self.active_tab_index { + Some(0) | None => self.tabs.len() - 1, + Some(index) => (index - 1) % self.tabs.len(), + }; + self.active_tab_index = Some(prev); + self.sync_needed = true; + cx.notify(); + } + + /// 跳到指定行(1-based,钳制到总行数;不置同步标记以免覆盖光标)。 + pub fn go_to_line(&mut self, line: u32, window: &mut Window, cx: &mut Context) { + if self.active_tab_index.is_none() { + return; + } + let total = self + .editor_state + .read(cx) + .value() + .to_string() + .split('\n') + .count() + .max(1) as u32; + let clamped = line.max(1).min(total); + self.editor_state.update(cx, |editor, cx| { + editor.set_cursor_position(Position::new(clamped - 1, 0), window, cx); + }); + self.sync_needed = false; + } + + /// 取反自动换行并同步设置落盘。 + pub fn toggle_wrap(&mut self, window: &mut Window, cx: &mut Context) { + self.soft_wrap = !self.soft_wrap; + let wrap = self.soft_wrap; + self.editor_state.update(cx, |editor, cx| { + editor.set_soft_wrap(wrap, window, cx); + }); + settings::update(cx, |s| s.word_wrap = wrap); + } + + /// 取反行号显示并同步设置落盘。 + pub fn toggle_line_numbers(&mut self, window: &mut Window, cx: &mut Context) { + self.show_line_numbers = !self.show_line_numbers; + let show = self.show_line_numbers; + self.editor_state.update(cx, |editor, cx| { + editor.set_line_number(show, window, cx); + }); + settings::update(cx, |s| s.line_numbers = show); + } + + /// 取反空白字符显示(无对应设置项,仅编辑器侧生效)。 + pub fn toggle_whitespace(&mut self, window: &mut Window, cx: &mut Context) { + self.show_whitespace = !self.show_whitespace; + let show = self.show_whitespace; + self.editor_state.update(cx, |editor, cx| { + editor.set_show_whitespaces(show, window, cx); + }); + } + + /// 获取推断的代码语言名称 + pub fn language_name(path: &str) -> &'static str { + if path.ends_with(".rs") { + "Rust" + } else if path.ends_with(".json") { + "JSON" + } else if path.ends_with(".toml") { + "TOML" + } else if path.ends_with(".md") { + "Markdown" + } else if path.ends_with(".ts") { + "TypeScript" + } else if path.ends_with(".js") { + "JavaScript" + } else if path.ends_with(".java") { + "Java" + } else if path.ends_with(".xml") { + "XML" + } else if path.ends_with(".yaml") || path.ends_with(".yml") { + "YAML" + } else if path.ends_with(".sh") { + "Shell" + } else if path.ends_with(".sql") { + "SQL" + } else { + "Plain Text" + } + } + + /// 按文件扩展名推断行注释风格(`language_name` 缺少 py/html/c 等分支,故直接匹配扩展名)。 + fn comment_style(path: &str) -> CommentStyle { + let ext = path.rsplit('.').next().unwrap_or("").to_ascii_lowercase(); + match ext.as_str() { + "py" | "sh" | "sql" | "toml" | "yaml" | "yml" | "rb" | "pl" => CommentStyle::Line("#"), + "html" | "xml" => CommentStyle::Block, + _ => CommentStyle::Line("//"), + } + } +} + +/// 编辑器标签派发的事件(右键菜单动作由 UI 层触发,父视图订阅处理)。 +#[derive(Debug, Clone)] +pub enum EditorTabEvent { + OpenInTerminal { dir: String }, + /// 活动标签的内容或身份发生变化,父视图据此同步 LSP 文档态。 + DocumentChanged { path: String, text: String }, +} + +impl EventEmitter for EditorView {} + +/// 行注释风格:行前缀或行级块包裹。 + +/// 行注释风格:行前缀或行级块包裹。 +enum CommentStyle { + Line(&'static str), + Block, +} + +fn is_code_file(name: &str) -> bool { + let lower = name.to_lowercase(); + lower.ends_with(".rs") + || lower.ends_with(".js") + || lower.ends_with(".ts") + || lower.ends_with(".jsx") + || lower.ends_with(".tsx") + || lower.ends_with(".json") + || lower.ends_with(".toml") + || lower.ends_with(".html") + || lower.ends_with(".css") + || lower.ends_with(".java") + || lower.ends_with(".c") + || lower.ends_with(".cpp") + || lower.ends_with(".h") + || lower.ends_with(".hpp") + || lower.ends_with(".py") + || lower.ends_with(".go") + || lower.ends_with(".swift") + || lower.ends_with(".sh") + || lower.ends_with(".xml") + || lower.ends_with(".yaml") + || lower.ends_with(".yml") + || lower.ends_with(".sql") +} + +impl Render for EditorView { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + self.sync_active_editor(window, cx); + // 设置弹窗可直接改 word_wrap/line_numbers/render_whitespace:每帧对齐 + // 全局设置(菜单 toggle 已同步,此处覆盖对话框路径)。 + { + let s = settings::get(cx); + let wrap = s.word_wrap; + let show_numbers = s.line_numbers; + let show_ws = s.render_whitespace.as_str() != "none"; + if wrap != self.soft_wrap { + self.soft_wrap = wrap; + self.editor_state.update(cx, |editor, cx| { + editor.set_soft_wrap(wrap, window, cx); + }); + } + if show_numbers != self.show_line_numbers { + self.show_line_numbers = show_numbers; + self.editor_state.update(cx, |editor, cx| { + editor.set_line_number(show_numbers, window, cx); + }); + } + if show_ws != self.show_whitespace { + self.show_whitespace = show_ws; + self.editor_state.update(cx, |editor, cx| { + editor.set_show_whitespaces(show_ws, window, cx); + }); + } + } + + let active_tab = self + .active_tab_index + .and_then(|idx| self.tabs.get(idx).cloned()); + let is_dark = { + let s = settings::get(cx); + !crate::theme::ThemePalette::is_light(&settings::resolved_theme_id(s, false)) + }; + + let editor_state = self.editor_state.clone(); + + v_flex() + .size_full() + .bg(ThemeColors::bg_editor()) + .child( + // 1. 顶部标签栏(Tab Bar,高 34px) + h_flex() + .h(px(34.0)) + .w_full() + .bg(ThemeColors::bg_tab_bar()) + .border_b_1() + .border_color(ThemeColors::border()) + .items_center() + .overflow_x_scrollbar() + .children(self.tabs.iter().enumerate().map(|(idx, tab)| { + let is_active = self.active_tab_index == Some(idx); + let title = tab.title.clone(); + let is_dirty = tab.is_dirty; + // 对齐 Tauri `shouldShowTabCloseButton`:固定常显, + // `always` 常显,`active` 仅当前显,其余悬停显。 + let visibility = + crate::settings::get(cx).tab_close_button_visibility.clone(); + let show_close = tab.is_pinned + || visibility == "always" + || (visibility == "active" && is_active); + let group_name = format!("tab-close-group-{idx}"); + + h_flex() + .id(idx) + .group(group_name.clone()) + .h(px(34.0)) + .items_center() + .gap_2() + .px_3() + .relative() + .cursor_pointer() + .when(is_active, |t| t.bg(ThemeColors::bg_editor())) + .when(!is_active, |t| { + t.bg(ThemeColors::bg_tab_bar()) + .hover(|h| h.bg(ThemeColors::bg_tab_hover())) + }) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.active_tab_index = Some(idx); + cx.notify(); + })) + .child( + if let Some(arc) = + crate::workbench::file_icon::file_image(&title, is_dark) + { + div() + .size(px(14.0)) + .child( + gpui_kit::img(gpui_kit::ImageSource::Image(arc)) + .size_full(), + ) + .into_any_element() + } else { + let icon_name = if is_code_file(&title) { + IconName::FileCode + } else { + IconName::FileText + }; + Icon::new(icon_name) + .size(px(14.0)) + .text_color(if is_active { + ThemeColors::accent_blue() + } else { + ThemeColors::text_muted() + }) + .into_any_element() + }, + ) + .child( + div() + .text_xs() + .text_color(if is_active { + ThemeColors::text_primary() + } else { + ThemeColors::text_muted() + }) + .child(title), + ) + .when(is_dirty, |t| { + t.child( + div() + .w(px(6.0)) + .h(px(6.0)) + .rounded_full() + .bg(ThemeColors::accent_blue()), + ) + }) + .child( + div() + .id(("close-tab", idx)) + .p(px(2.0)) + .rounded_sm() + .cursor_pointer() + .when(!show_close, |d| { + d.opacity(0.0) + .group_hover(group_name, |style| style.opacity(1.0)) + }) + .hover(|h| { + h.bg(ThemeColors::bg_tab_hover()) + .text_color(ThemeColors::accent_red()) + }) + .child( + Icon::new(IconName::Close) + .size(px(12.0)) + .text_color(ThemeColors::text_muted()), + ) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.close_tab(idx, cx); + })), + ) + // 激活 Tab 底部 2px 高亮条(对齐 macOS tabUnderline) + .when(is_active, |t| { + t.child( + div() + .absolute() + .bottom_0() + .left_0() + .right_0() + .h(px(2.0)) + .bg(ThemeColors::accent_blue()), + ) + }) + // 右键记 `context_menu_tab` 并弹出标签菜单(上游 + // `context_menu` 管显隐,浮层不占布局;点选后由 + // 菜单动作清 `None`,点外由下式清 `None`)。 + .on_mouse_down( + gpui_kit::MouseButton::Right, + cx.listener(move |this, _event, _window, cx| { + this.context_menu_tab = Some(idx); + cx.notify(); + }), + ) + .on_mouse_down_out(cx.listener(|this, _event, _window, cx| { + if this.context_menu_tab.is_some() { + this.context_menu_tab = None; + cx.notify(); + } + })) + .context_menu({ + let view = cx.entity(); + let pane_id = self.pane_id.unwrap_or(0); + let locked = self.pane_locked; + let on_split: SplitCallback = self + .on_split + .clone() + .unwrap_or_else(|| Rc::new(|_, _, _, _| {})); + let on_toggle_lock: ToggleLockCallback = self + .on_toggle_lock + .clone() + .unwrap_or_else(|| Rc::new(|_, _| {})); + crate::workbench::tab_menu::tab_context_menu( + view, + idx, + pane_id, + locked, + on_split, + on_toggle_lock, + ) + }) + })), + ) + .when_some(active_tab.as_ref(), |this, tab| { + // 2. 面包屑导航栏(Breadcrumb Bar,高 24px) + let segments: Vec<&str> = tab.path.split('/').filter(|s| !s.is_empty()).collect(); + let last_idx = segments.len().saturating_sub(1); + + this.child( + h_flex() + .h(px(24.0)) + .w_full() + .bg(ThemeColors::bg_tab_active()) + .border_b_1() + .border_color(ThemeColors::border()) + .items_center() + .px_3() + .gap_1p5() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child( + Icon::new(IconName::Folder) + .size(px(12.0)) + .text_color(ThemeColors::text_muted()), + ) + .children(segments.into_iter().enumerate().flat_map(|(idx, seg)| { + let is_last = idx == last_idx; + let seg_element = div() + .text_xs() + .text_color(if is_last { + ThemeColors::text_primary() + } else { + ThemeColors::text_muted() + }) + .child(seg.to_string()) + .into_any_element(); + + if is_last { + vec![seg_element] + } else { + vec![ + seg_element, + Icon::new(IconName::ChevronRight) + .size(px(10.0)) + .text_color(ThemeColors::text_muted()) + .into_any_element(), + ] + } + })), + ) + }) + .child( + // 3. 中央代码内容区(滚动与文本选择由上游 Editor 负责) + div() + .flex_1() + .w_full() + .overflow_hidden() + .when_some( + active_tab.as_ref().map(|_| editor_state.clone()), + |this, state| { + this.child( + Editor::new(&state) + .h(relative(1.0)) + .bordered(false) + .readonly(false) + .text_size(px(crate::settings::get(cx).font_size)), + ) + }, + ) + .when(active_tab.is_none(), |this| { + // 无打开文件空态,对齐 Tauri `EmptyEditorState`: + // 文件图标 + 右下角放大镜叠加,标题与描述使用 i18n 同款文案。 + this.child( + h_flex() + .size_full() + .items_center() + .justify_center() + .bg(ThemeColors::bg_editor()) + .px_6() + .py_8() + .child( + v_flex() + .max_w(px(448.0)) + .items_center() + .gap_3() + .child( + div() + .relative() + .size(px(48.0)) + .flex() + .items_center() + .justify_center() + .text_color(ThemeColors::text_muted()) + .child(Icon::new(IconName::FileText).size(px(40.0))) + .child( + div().absolute().bottom_0().right_0().child( + Icon::new(IconName::Search).size(px(20.0)), + ), + ), + ) + .child( + div() + .text_base() + .font_weight(FontWeight::MEDIUM) + .text_color(ThemeColors::text_primary()) + .child("选择文件以查看"), + ) + .child( + div() + .text_sm() + .text_color(ThemeColors::text_muted()) + .child("外部工具产生的更改会自动显示。"), + ), + ), + ) + }), + ) + } +} diff --git a/linux/src/workbench/extensions_panel.rs b/linux/src/workbench/extensions_panel.rs new file mode 100644 index 000000000..8e6cf0065 --- /dev/null +++ b/linux/src/workbench/extensions_panel.rs @@ -0,0 +1,954 @@ +//! 右侧扩展面板:搜索 + 分类签 + 列表点选 + 详情,对齐 Tauri +//! `windows/tauri/src/extensions/ui/components/extensions-sidebar.tsx` +//! 的展示语义(标题计数、分类签计数、行点选、详情徽标与主操作)。 +//! +//! 平台适配说明:Tauri 的扩展页在中央开 buffer 左右两栏展示,Linux 中央是 +//! 编辑器 Tab 且右侧面板较窄,因此列表与详情纵向堆叠(列表上、详情下); +//! 安装/更新/主题选用等需要远端市场与外观系统的能力暂不提供,主操作仅为 +//! 启用/停用(落盘到 `~/.config/lithe/extensions.json`)。头部 X 只发射 +//! [`ExtensionsEvent::Close`],由外部(`view.rs`)决定面板显隐,与 +//! [`crate::workbench::maven::MavenView`] 一致。 + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use gpui_kit::assets::IconName; +use gpui_kit::component::button::{Button, ButtonVariants as _}; +use gpui_kit::component::input::{Input, InputEvent, InputState}; +use gpui_kit::component::scroll::ScrollableElement as _; +use gpui_kit::component::{h_flex, v_flex, Icon, Sizable as _}; +use gpui_kit::prelude::FluentBuilder as _; +use gpui_kit::{ + div, px, AnyElement, AppContext as _, Context, Entity, EventEmitter, FontWeight, + InteractiveElement as _, IntoElement, ParentElement as _, Render, + StatefulInteractiveElement as _, Styled as _, Subscription, Window, +}; + +use crate::theme::ThemeColors; + +/// 扩展分类:与 Tauri `FILTER_TABS` 的 9 个 tab id 逐一对齐。 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ExtensionCategory { + #[default] + All, + Language, + Theme, + IconTheme, + Database, + Ai, + Integration, + Skill, + Agent, +} + +impl ExtensionCategory { + /// 分类签的固定顺序,与 Tauri `FILTER_TABS` 一致。 + pub const ALL: [ExtensionCategory; 9] = [ + ExtensionCategory::All, + ExtensionCategory::Language, + ExtensionCategory::Theme, + ExtensionCategory::IconTheme, + ExtensionCategory::Database, + ExtensionCategory::Ai, + ExtensionCategory::Integration, + ExtensionCategory::Skill, + ExtensionCategory::Agent, + ]; + + /// 稳定 id,与 Tauri 的 tab id 对齐。 + pub fn as_str(self) -> &'static str { + match self { + ExtensionCategory::All => "all", + ExtensionCategory::Language => "language", + ExtensionCategory::Theme => "theme", + ExtensionCategory::IconTheme => "icon-theme", + ExtensionCategory::Database => "database", + ExtensionCategory::Ai => "ai", + ExtensionCategory::Integration => "integration", + ExtensionCategory::Skill => "skill", + ExtensionCategory::Agent => "agent", + } + } + + /// 分类签图标:对齐 Tauri 各 tab 图标;`all` 无图标。 + pub fn icon(self) -> Option { + match self { + ExtensionCategory::All => None, + ExtensionCategory::Language => Some(IconName::Languages), + ExtensionCategory::Theme => Some(IconName::Paintbrush), + ExtensionCategory::IconTheme => Some(IconName::Package), + ExtensionCategory::Database => Some(IconName::Database), + ExtensionCategory::Ai => Some(IconName::Sparkles), + ExtensionCategory::Integration => Some(IconName::Plug), + ExtensionCategory::Skill => Some(IconName::Brain), + ExtensionCategory::Agent => Some(IconName::Bot), + } + } + + fn from_str(id: &str) -> Option { + match id { + "all" => Some(ExtensionCategory::All), + "language" => Some(ExtensionCategory::Language), + "theme" => Some(ExtensionCategory::Theme), + "icon-theme" => Some(ExtensionCategory::IconTheme), + "database" => Some(ExtensionCategory::Database), + "ai" => Some(ExtensionCategory::Ai), + "integration" => Some(ExtensionCategory::Integration), + "skill" => Some(ExtensionCategory::Skill), + "agent" => Some(ExtensionCategory::Agent), + _ => None, + } + } +} + +/// 单个扩展条目:仓库插件扫描结果或内置项(本地均视为已安装)。 +#[derive(Debug, Clone)] +pub struct ExtensionItem { + /// 稳定 id:`package.json` 的 name、`plugin.json` 的 id 或内置 id。 + pub id: String, + pub name: String, + pub version: String, + pub description: String, + pub category: ExtensionCategory, + /// 发布者:`package.json` 的 publisher/author,缺失为 `None`(不展示)。 + pub publisher: Option, + /// 是否随产品发布(对齐 Tauri `isBundled` 徽标)。 + pub is_bundled: bool, +} + +/// 扩展面板派发的事件。 +#[derive(Debug, Clone)] +pub enum ExtensionsEvent { + Close, +} + +/// 右侧扩展面板:搜索框 + 分类签 + 列表点选 + 详情。 +pub struct ExtensionsView { + items: Vec, + /// 启用状态 `{id: bool}`,缺省视为启用,落盘到 + /// `~/.config/lithe/extensions.json`。 + enabled: HashMap, + search_input: Option>, + _search_subscription: Option, + category: ExtensionCategory, + /// 选中的扩展 id(对齐 Tauri `selectedExtensionId`;过滤后不在列表 + /// 则展示回退到首项,不在此处改写)。 + selected: Option, +} + +impl EventEmitter for ExtensionsView {} + +impl ExtensionsView { + pub fn new(_cx: &mut Context) -> Self { + let mut view = Self { + items: Vec::new(), + enabled: load_enabled(), + search_input: None, + _search_subscription: None, + category: ExtensionCategory::All, + selected: None, + }; + view.rescan(); + view + } + + /// 重新扫描仓库插件(内置项常驻);选中项消失则清空选中。 + pub fn rescan(&mut self) { + let mut items = builtin_items(); + items.extend(scan_plugins()); + // 按 id 去重(扫描到的优先),再按名称稳定排序。 + let mut seen = std::collections::HashSet::new(); + items.retain(|item| seen.insert(item.id.clone())); + items.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); + if let Some(selected) = self.selected.clone() { + if !items.iter().any(|item| item.id == selected) { + self.selected = None; + } + } + self.items = items; + } + + pub fn is_enabled(&self, id: &str) -> bool { + self.enabled.get(id).copied().unwrap_or(true) + } + + fn toggle(&mut self, id: &str, cx: &mut Context) { + let next = !self.is_enabled(id); + self.enabled.insert(id.to_string(), next); + save_enabled(&self.enabled); + cx.notify(); + } + + /// 懒创建搜索输入框(参考 `settings_dialog.rs::ensure_input`)。 + fn ensure_search_input( + slot: &mut Option>, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + if let Some(entity) = slot.clone() { + return entity; + } + let entity = cx.new(|cx| InputState::new(window, cx)); + *slot = Some(entity.clone()); + entity + } + + fn search_query(&self, cx: &mut Context) -> String { + self.search_input + .as_ref() + .map(|e| e.read(cx).value().to_string()) + .unwrap_or_default() + } + + /// 搜索过滤(先搜索后分签,对齐 Tauri `searchMatchedExtensions`)。 + fn search_matched(&self, query: &str) -> Vec<&ExtensionItem> { + let q = query.trim().to_lowercase(); + self.items + .iter() + .filter(|item| { + q.is_empty() + || item.name.to_lowercase().contains(&q) + || item.description.to_lowercase().contains(&q) + || item.id.to_lowercase().contains(&q) + }) + .collect() + } + + /// 各分类签计数(搜索命中内部分类计数,对齐 Tauri `filterCounts`)。 + fn tab_counts(&self, matched: &[&ExtensionItem]) -> [usize; 9] { + let mut counts = [0usize; 9]; + for (ix, category) in ExtensionCategory::ALL.iter().enumerate() { + counts[ix] = if *category == ExtensionCategory::All { + matched.len() + } else { + matched + .iter() + .filter(|item| item.category == *category) + .count() + }; + } + counts + } + + /// 分类签行:横滑签 + 计数角标,对齐 Tauri `FILTER_TABS` 行。 + fn render_tab_row(&self, counts: &[usize; 9], cx: &mut Context) -> impl IntoElement { + h_flex() + .w_full() + .gap_1() + .overflow_x_scrollbar() + .py_1() + .children( + ExtensionCategory::ALL + .iter() + .enumerate() + .map(|(ix, category)| { + let count = counts[ix]; + let active = self.category == *category; + let id = category.as_str().to_string(); + h_flex() + .id(format!("ext-tab-{id}")) + .flex_shrink_0() + .items_center() + .gap_1p5() + .h(px(28.0)) + .px_2p5() + .rounded_sm() + .cursor_pointer() + .text_xs() + .when(active, |el| { + el.bg(ThemeColors::subtle_selection()) + .text_color(ThemeColors::text_primary()) + }) + .when(!active, |el| { + el.text_color(ThemeColors::text_muted()).hover(|h| { + h.bg(ThemeColors::bg_tab_hover()) + .text_color(ThemeColors::text_primary()) + }) + }) + .when_some(category.icon(), |el, icon| { + el.child(Icon::new(icon).size(px(13.0)).text_color(if active { + ThemeColors::text_primary() + } else { + ThemeColors::text_muted() + })) + }) + .child(category_label(*category, cx)) + .child( + div() + .px_1() + .rounded_sm() + .text_xs() + .bg(if active { + ThemeColors::accent_blue() + } else { + ThemeColors::bg_tab_hover() + }) + .text_color(if active { + ThemeColors::foreground() + } else { + ThemeColors::text_muted() + }) + .child(count.to_string()), + ) + .on_click(cx.listener(move |this, _event, _window, cx| { + if let Some(category) = ExtensionCategory::from_str(&id) { + this.category = category; + cx.notify(); + } + })) + }), + ) + } + + /// 状态徽标:分类 / 已安装 / 已禁用 / 内置,对齐 Tauri 详情徽标行。 + fn render_badge(text: String, highlighted: bool) -> AnyElement { + div() + .px_1p5() + .py_0p5() + .rounded_sm() + .border_1() + .text_xs() + .border_color(if highlighted { + ThemeColors::accent_blue() + } else { + ThemeColors::border() + }) + .text_color(if highlighted { + ThemeColors::accent_blue() + } else { + ThemeColors::text_muted() + }) + .child(text) + .into_any_element() + } + + /// 详情区:图标 + 名 + 发布者/版本 + 徽标 + 描述 + 主操作 + 贡献项, + /// 对齐 Tauri 右侧详情栏(纵向堆叠适配窄面板)。 + fn render_detail(&self, item: &ExtensionItem, cx: &mut Context) -> AnyElement { + let enabled = self.is_enabled(&item.id); + let item_id = item.id.clone(); + let mut badges = vec![Self::render_badge(category_label(item.category, cx), false)]; + badges.push(Self::render_badge( + crate::i18n::menu_text(cx, "extensions.installed").to_string(), + true, + )); + if !enabled { + badges.push(Self::render_badge( + crate::i18n::menu_text(cx, "extensions.disabled").to_string(), + false, + )); + } + if item.is_bundled { + badges.push(Self::render_badge( + crate::i18n::menu_text(cx, "extensions.builtIn").to_string(), + true, + )); + } + let by_publisher = item.publisher.clone().map(|publisher| { + crate::i18n::menu_text(cx, "extensions.byPublisher").replace("{publisher}", &publisher) + }); + v_flex() + .w_full() + .gap_2() + .p_3() + .child( + h_flex() + .w_full() + .items_center() + .gap_2p5() + .child( + div() + .flex_shrink_0() + .size(px(40.0)) + .flex() + .items_center() + .justify_center() + .rounded_md() + .border_1() + .border_color(ThemeColors::border()) + .bg(ThemeColors::background()) + .child( + Icon::new(item.category.icon().unwrap_or(IconName::Puzzle)) + .size(px(18.0)) + .text_color(ThemeColors::text_muted()), + ), + ) + .child( + v_flex() + .flex_1() + .min_w_0() + .child( + div() + .truncate() + .text_sm() + .font_weight(FontWeight::BOLD) + .text_color(ThemeColors::text_primary()) + .child(item.name.clone()), + ) + .when_some(by_publisher, |el, text| { + el.child( + div() + .truncate() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(text), + ) + }) + .when(!item.version.is_empty(), |el| { + el.child( + div() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(format!("v{}", item.version)), + ) + }), + ), + ) + .child(h_flex().w_full().flex_wrap().gap_1p5().children(badges)) + .when(!item.description.is_empty(), |el| { + el.child( + div() + .w_full() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(item.description.clone()), + ) + }) + .child( + Button::new(format!("ext-detail-toggle-{}", item.id)) + .small() + .when(enabled, |b| b.ghost()) + .when(!enabled, |b| b.primary()) + .icon(if enabled { + IconName::Close + } else { + IconName::Check + }) + .label(if enabled { + crate::i18n::menu_text(cx, "extensions.deactivate").to_string() + } else { + crate::i18n::menu_text(cx, "extensions.activate").to_string() + }) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.toggle(&item_id, cx); + })), + ) + .child( + v_flex() + .w_full() + .gap_1p5() + .child( + div() + .text_xs() + .font_weight(FontWeight::BOLD) + .text_color(ThemeColors::text_primary()) + .child(crate::i18n::menu_text(cx, "extensions.contributions")), + ) + .child( + h_flex() + .w_full() + .flex_wrap() + .gap_1p5() + .child(Self::render_badge(category_label(item.category, cx), false)), + ), + ) + .into_any_element() + } +} + +/// 分类签文案:`extensions.*` 与 Tauri `locale.ts` 同键。 +fn category_label(category: ExtensionCategory, cx: &gpui_kit::App) -> String { + let key = match category { + ExtensionCategory::All => "extensions.all", + ExtensionCategory::Language => "extensions.languages", + ExtensionCategory::Theme => "extensions.themes", + ExtensionCategory::IconTheme => "extensions.iconThemes", + ExtensionCategory::Database => "extensions.databases", + ExtensionCategory::Ai => "extensions.ai", + ExtensionCategory::Integration => "extensions.integrations", + ExtensionCategory::Skill => "extensions.skills", + ExtensionCategory::Agent => "extensions.agents", + }; + crate::i18n::menu_text(cx, key).to_string() +} + +impl Render for ExtensionsView { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + // 搜索框懒创建 + 一次性订阅 Change 事件,保证输入时列表实时过滤。 + let search_entity = Self::ensure_search_input(&mut self.search_input, window, cx); + if self._search_subscription.is_none() { + self._search_subscription = Some(cx.subscribe( + &search_entity, + |_this: &mut Self, _, event: &InputEvent, cx| { + if matches!(event, InputEvent::Change) { + cx.notify(); + } + }, + )); + } + let query = self.search_query(cx); + let matched = self.search_matched(&query); + let counts = self.tab_counts(&matched); + let filtered: Vec<&ExtensionItem> = matched + .iter() + .filter(|item| { + self.category == ExtensionCategory::All || item.category == self.category + }) + .copied() + .collect(); + // 选中不在过滤结果内则回退首项(对齐 Tauri `selectedExtension` 回退)。 + let selected_id = self + .selected + .clone() + .filter(|id| filtered.iter().any(|item| item.id == *id)) + .or_else(|| filtered.first().map(|item| item.id.clone())); + let installed = self.items.len(); + let subtitle = format!( + "{} · {}", + crate::i18n::menu_text(cx, "extensions.availableCount") + .replace("{count}", &filtered.len().to_string()), + crate::i18n::menu_text(cx, "extensions.installedCount") + .replace("{count}", &installed.to_string()), + ); + + let mut rows = Vec::new(); + for item in &filtered { + let enabled = self.is_enabled(&item.id); + let selected = selected_id.as_deref() == Some(item.id.as_str()); + let id_owned = item.id.clone(); + rows.push( + h_flex() + .id(format!("ext-row-{}", item.id)) + .w_full() + .items_center() + .gap_2p5() + .px_2() + .py_1p5() + .rounded_md() + .cursor_pointer() + .when(selected, |el| { + el.bg(ThemeColors::subtle_selection()) + .text_color(ThemeColors::text_primary()) + }) + .when(!selected, |el| { + el.hover(|h| h.bg(ThemeColors::bg_tab_hover())) + }) + .child( + div() + .flex_shrink_0() + .size(px(36.0)) + .flex() + .items_center() + .justify_center() + .rounded_md() + .border_1() + .border_color(ThemeColors::border()) + .bg(ThemeColors::background()) + .child( + Icon::new(item.category.icon().unwrap_or(IconName::Puzzle)) + .size(px(16.0)) + .text_color(ThemeColors::text_muted()), + ), + ) + .child( + v_flex() + .flex_1() + .min_w_0() + .child( + div() + .truncate() + .text_xs() + .font_weight(FontWeight::BOLD) + .text_color(ThemeColors::text_primary()) + .child(item.name.clone()), + ) + .child( + div() + .text_xs() + .truncate() + .text_color(ThemeColors::text_muted()) + .child(item.description.clone()), + ), + ) + .child(if enabled { + div() + .flex_shrink_0() + .size(px(28.0)) + .flex() + .items_center() + .justify_center() + .text_color(ThemeColors::text_muted()) + .child( + Icon::new(IconName::Check) + .size(px(14.0)) + .text_color(ThemeColors::text_muted()), + ) + .into_any_element() + } else { + Button::new(format!("ext-install-{}", item.id)) + .small() + .primary() + .icon(IconName::Plus) + .tooltip(crate::i18n::menu_text(cx, "extensions.activate")) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.enabled.insert(id_owned.clone(), true); + save_enabled(&this.enabled); + cx.notify(); + })) + .into_any_element() + }) + .on_click(cx.listener({ + let id_owned = item.id.clone(); + move |this, _event, _window, cx| { + this.selected = Some(id_owned.clone()); + cx.notify(); + } + })) + .into_any_element(), + ); + } + let detail = selected_id + .as_deref() + .and_then(|id| filtered.iter().find(|item| item.id == id)) + .map(|item| self.render_detail(item, cx)); + + v_flex() + .size_full() + .bg(ThemeColors::bg_sidebar()) + .border_l_1() + .border_color(ThemeColors::border()) + .child( + h_flex() + .h(px(32.0)) + .w_full() + .bg(ThemeColors::bg_sidebar()) + .border_b_1() + .border_color(ThemeColors::border()) + .items_center() + .justify_between() + .px_3() + .child( + v_flex() + .flex_1() + .min_w_0() + .child( + div() + .text_xs() + .font_weight(FontWeight::BOLD) + .text_color(ThemeColors::text_muted()) + .child(crate::i18n::menu_text(cx, "extensions.title")), + ) + .child( + div() + .truncate() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(subtitle), + ), + ) + .child( + Button::new("extensions-close") + .small() + .ghost() + .icon(IconName::Close) + .tooltip(crate::i18n::menu_text(cx, "ui.close")) + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(ExtensionsEvent::Close); + })), + ), + ) + .child( + v_flex() + .w_full() + .flex_shrink_0() + .gap_1() + .px_2() + .pt_2() + .child( + div() + .w_full() + .child(Input::new(&search_entity).cleanable(true)), + ) + .child(self.render_tab_row(&counts, cx)), + ) + .child(if rows.is_empty() { + div() + .flex_1() + .w_full() + .flex() + .items_center() + .justify_center() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(crate::i18n::menu_text(cx, "extensions.noneFound")) + .into_any_element() + } else { + div() + .flex_1() + .w_full() + .min_h_0() + .overflow_y_scrollbar() + .px_1() + .children(rows) + .into_any_element() + }) + .when_some(detail, |el, detail| { + el.child( + div() + .w_full() + .flex_shrink_0() + .max_h(px(340.0)) + .overflow_y_scrollbar() + .border_t_1() + .border_color(ThemeColors::border()) + .bg(ThemeColors::bg_sidebar()) + .child(detail), + ) + }) + } +} + +/// 内置项:随产品发布,不参与仓库扫描。 +fn builtin_items() -> Vec { + vec![ + ExtensionItem { + id: "git".to_string(), + name: "Git".to_string(), + version: "0.1.0".to_string(), + description: "Git source control integration".to_string(), + category: ExtensionCategory::Integration, + publisher: None, + is_bundled: true, + }, + ExtensionItem { + id: "maven".to_string(), + name: "Maven".to_string(), + version: "0.1.0".to_string(), + description: "Maven project navigation and lifecycle".to_string(), + category: ExtensionCategory::Integration, + publisher: None, + is_bundled: true, + }, + ExtensionItem { + id: "terminal".to_string(), + name: "Terminal".to_string(), + version: "0.1.0".to_string(), + description: "Integrated terminal".to_string(), + category: ExtensionCategory::Integration, + publisher: None, + is_bundled: true, + }, + ] +} + +/// 扫描仓库 `Plugins/` 目录:每个子目录优先读 `package.json`,缺失则读 +/// `plugin.json`(仓库现状),二者皆无则跳过(由父目录继续下钻)。 +/// 最大递归深度 4,跳过隐藏目录与构建产物目录。 +fn scan_plugins() -> Vec { + let Some(root) = plugins_root() else { + return Vec::new(); + }; + let mut out = Vec::new(); + walk_plugins_dir(&root, 0, &mut out); + out +} + +fn plugins_root() -> Option { + let mut candidates = Vec::new(); + if let Ok(cwd) = std::env::current_dir() { + candidates.push(cwd.join("Plugins")); + } + if let Ok(exe) = std::env::current_exe() { + let mut dir = exe.parent().map(Path::to_path_buf); + for _ in 0..6 { + let Some(current) = dir.clone() else { + break; + }; + candidates.push(current.join("Plugins")); + dir = current.parent().map(Path::to_path_buf); + } + } + candidates.into_iter().find(|path| path.is_dir()) +} + +const MAX_PLUGIN_SCAN_DEPTH: usize = 4; + +const PLUGIN_SCAN_SKIP_DIRS: [&str; 7] = [ + "target", + "node_modules", + ".git", + "dist", + "build", + "out", + "vendor", +]; + +fn walk_plugins_dir(dir: &Path, depth: usize, out: &mut Vec) { + if depth > MAX_PLUGIN_SCAN_DEPTH { + return; + } + let Ok(entries) = std::fs::read_dir(dir) else { + return; + }; + let mut entries: Vec<_> = entries.flatten().collect(); + entries.sort_by_key(|entry| entry.file_name()); + for entry in entries { + let path = entry.path(); + if !path.is_dir() { + continue; + } + let name = entry.file_name().to_string_lossy().into_owned(); + if name.starts_with('.') || PLUGIN_SCAN_SKIP_DIRS.contains(&name.as_str()) { + continue; + } + if let Some(item) = read_manifest(&path, &name) { + out.push(item); + } + walk_plugins_dir(&path, depth + 1, out); + } +} + +#[derive(Debug, serde::Deserialize)] +struct PackageJson { + name: Option, + version: Option, + description: Option, + keywords: Option>, + publisher: Option, + author: Option, +} + +#[derive(Debug, serde::Deserialize)] +struct PluginJson { + id: Option, + #[serde(rename = "displayName")] + display_name: Option, + version: Option, + description: Option, + #[serde(rename = "languageSupports")] + language_supports: Option>, +} + +/// 读单个插件目录的清单:`package.json` 优先,`plugin.json` 兜底, +/// 字段缺失时用目录名;`plugin.json` 含非空 `languageSupports` 即归语言类。 +fn read_manifest(dir: &Path, dir_name: &str) -> Option { + let package_path = dir.join("package.json"); + if package_path.is_file() { + let text = std::fs::read_to_string(&package_path).ok()?; + let manifest: PackageJson = serde_json::from_str(&text).ok()?; + let keywords = manifest.keywords.clone().unwrap_or_default(); + let publisher = manifest + .publisher + .filter(|s| !s.trim().is_empty()) + .or_else(|| manifest.author.filter(|s| !s.trim().is_empty())); + return Some(ExtensionItem { + id: manifest + .name + .clone() + .unwrap_or_else(|| dir_name.to_string()), + name: manifest.name.unwrap_or_else(|| dir_name.to_string()), + version: manifest.version.unwrap_or_default(), + description: manifest.description.unwrap_or_default(), + category: classify_keywords(&keywords), + publisher, + is_bundled: false, + }); + } + let plugin_path = dir.join("plugin.json"); + if plugin_path.is_file() { + let text = std::fs::read_to_string(&plugin_path).ok()?; + let manifest: PluginJson = serde_json::from_str(&text).ok()?; + let category = match &manifest.language_supports { + Some(supports) if !supports.is_empty() => ExtensionCategory::Language, + _ => classify_text(&format!( + "{} {}", + manifest.id.clone().unwrap_or_default(), + manifest.display_name.clone().unwrap_or_default() + )), + }; + return Some(ExtensionItem { + id: manifest.id.clone().unwrap_or_else(|| dir_name.to_string()), + name: manifest + .display_name + .or(manifest.id) + .unwrap_or_else(|| dir_name.to_string()), + version: manifest.version.unwrap_or_default(), + description: manifest.description.unwrap_or_default(), + category, + publisher: None, + is_bundled: false, + }); + } + None +} + +/// 按 `package.json` keywords 简单归类,无 keywords 归 integration。 +fn classify_keywords(keywords: &[String]) -> ExtensionCategory { + classify_text(&keywords.join(" ")) +} + +/// 关键词归类:theme/icon-theme → 主题系,language/lang/lsp/syntax → +/// 语言,database/sql → 数据库,ai/llm/agent/chat/model → AI, +/// skill → 技能,其余 → integration(对齐 Tauri 9 分类)。 +fn classify_text(text: &str) -> ExtensionCategory { + let lower = text.to_lowercase(); + let has_any = |words: &[&str]| words.iter().any(|word| lower.contains(word)); + if has_any(&["icon-theme", "icon-pack", "file-icon"]) { + ExtensionCategory::IconTheme + } else if has_any(&["theme", "icon", "color-scheme"]) { + ExtensionCategory::Theme + } else if has_any(&["language", "lang", "lsp", "syntax", "grammar"]) { + ExtensionCategory::Language + } else if has_any(&[ + "database", "sql", "sqlite", "postgres", "mysql", "mongo", "redis", + ]) { + ExtensionCategory::Database + } else if has_any(&["skill"]) { + ExtensionCategory::Skill + } else if has_any(&["agent", "robot"]) { + ExtensionCategory::Agent + } else if has_any(&["ai", "llm", "chat", "model"]) { + ExtensionCategory::Ai + } else { + ExtensionCategory::Integration + } +} + +/// 启用状态落盘路径:`~/.config/lithe/extensions.json` +///(`$XDG_CONFIG_HOME` 优先,与 `settings.rs::config_path` 同规则)。 +fn extensions_state_path() -> Option { + let base = std::env::var_os("XDG_CONFIG_HOME") + .map(PathBuf::from) + .filter(|path| !path.as_os_str().is_empty()) + .or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".config")))?; + Some(base.join("lithe").join("extensions.json")) +} + +fn load_enabled() -> HashMap { + let Some(path) = extensions_state_path() else { + return HashMap::new(); + }; + let Ok(text) = std::fs::read_to_string(&path) else { + return HashMap::new(); + }; + serde_json::from_str::>(&text).unwrap_or_default() +} + +/// 写回启用状态:临时文件 + 原子替换(参考 `settings.rs::persist`)。 +fn save_enabled(enabled: &HashMap) { + let Some(path) = extensions_state_path() else { + return; + }; + if let Some(parent) = path.parent() { + if std::fs::create_dir_all(parent).is_err() { + return; + } + } + let Ok(text) = serde_json::to_string_pretty(enabled) else { + return; + }; + let tmp = path.with_extension("json.tmp"); + if std::fs::write(&tmp, text).is_ok() { + let _ = std::fs::rename(&tmp, &path); + } +} diff --git a/linux/src/workbench/file_icon.rs b/linux/src/workbench/file_icon.rs new file mode 100644 index 000000000..2c102ac00 --- /dev/null +++ b/linux/src/workbench/file_icon.rs @@ -0,0 +1,107 @@ +//! 工作台文件/目录图标查找:复刻 Windows 图标主题语义。 +//! +//! basename 转小写后先查 `filenames` 精确匹配,再按 `.` 切分做多后缀候选 +//! (如 `a.d.ts` 依次试 `.d.ts`、`.ts`,空首段无候选)查 `fileExtensions`; +//! 目录按小写目录名查 `folders`/`expandedFolders`;都未命中用默认图标; +//! 浅色主题优先 `lightIconDefinitions` 同 id 的路径,无则回退深色路径。 +//! 查找表与 SVG 字节由 `linux/build.rs` 在编译期生成,运行期只做查表、 +//! 构造与进程级缓存。 + +use std::collections::HashMap; +use std::sync::{Arc, Mutex, OnceLock}; + +include!(concat!(env!("OUT_DIR"), "/file_icons_generated.rs")); + +/// 进程级解码图标缓存:键为(图标定义 id,是否深色主题)。 +static ICON_CACHE: OnceLock>>> = OnceLock::new(); + +/// 取路径最后一段,同时处理 `/` 与 `\`(与 Windows 端 basename 语义一致)。 +fn basename(path: &str) -> &str { + path.rsplit(|c| c == '/' || c == '\\') + .next() + .unwrap_or(path) +} + +/// 转小写待查名;basename 为空时回退整串转小写(对齐 Windows 端的 `||` 兜底)。 +fn lookup_name(path: &str) -> String { + let base = basename(path); + if base.is_empty() { + path.to_lowercase() + } else { + base.to_lowercase() + } +} + +/// 多后缀候选:`a.d.ts` 依次产出 `.d.ts`、`.ts`;首段为空(点文件)时无候选。 +fn extension_candidates(lower_name: &str) -> Vec { + let parts: Vec<&str> = lower_name.split('.').collect(); + if parts.len() < 2 || parts[0].is_empty() { + return Vec::new(); + } + (1..parts.len()) + .map(|i| format!(".{}", parts[i..].join("."))) + .collect() +} + +/// 按定义 id 取 SVG 字节并构造缓存的 [`gpui_kit::Image`];SVG 缺失返回 `None`。 +/// +/// 浅色主题先查浅色 definition,无专属路径时回退深色路径。 +fn load_icon(def: &str, is_dark: bool) -> Option> { + if def.is_empty() { + return None; + } + let cache = ICON_CACHE.get_or_init(|| Mutex::new(HashMap::new())); + let key = (def.to_string(), is_dark); + if let Some(hit) = cache.lock().expect("文件图标缓存锁中毒").get(&key) { + return Some(hit.clone()); + } + let bytes = svg_bytes_for_def(def, is_dark) + .or_else(|| (!is_dark).then(|| svg_bytes_for_def(def, true)).flatten())?; + let image = Arc::new(gpui_kit::Image::from_bytes( + gpui_kit::ImageFormat::Svg, + bytes.to_vec(), + )); + cache + .lock() + .expect("文件图标缓存锁中毒") + .insert(key, image.clone()); + Some(image) +} + +/// 按文件名查找文件图标;只取 basename,未命中任何表时用默认文件图标。 +/// +/// 入参与查表键均转小写匹配;对应 SVG 缺失时返回 `None`。 +pub fn file_image(file_name: &str, is_dark: bool) -> Option> { + let name = lookup_name(file_name); + let def = filename_def(&name) + .or_else(|| { + extension_candidates(&name) + .iter() + .find_map(|ext| file_extension_def(ext)) + }) + .unwrap_or_else(default_file_def); + load_icon(def, is_dark) +} + +/// 按目录名查找目录图标;只取 basename,未命中时用默认目录图标。 +/// +/// `expanded` 为真时优先查 `expandedFolders`,再查 `folders`,最后用 +/// `defaultFolderOpen`(缺失则用 `defaultFolder`);为假时只查 `folders`。 +pub fn folder_image(dir_name: &str, expanded: bool, is_dark: bool) -> Option> { + let name = lookup_name(dir_name); + let def = if expanded { + expanded_folder_def(&name) + .or_else(|| folder_def(&name)) + .unwrap_or_else(|| { + let open = default_folder_open_def(); + if open.is_empty() { + default_folder_def() + } else { + open + } + }) + } else { + folder_def(&name).unwrap_or_else(default_folder_def) + }; + load_icon(def, is_dark) +} diff --git a/linux/src/workbench/go_to_line.rs b/linux/src/workbench/go_to_line.rs new file mode 100644 index 000000000..d029abac4 --- /dev/null +++ b/linux/src/workbench/go_to_line.rs @@ -0,0 +1,219 @@ +//! 跳转到行(Go to Line)小弹窗。 +//! +//! 对齐 Tauri `editor.goToLine` 入口:数字输入 + Enter 确认,确认后通过 +//! [`GoToLineEvent::Confirm`] 把 1-based 行号交给上层,由 `EditorView::go_to_line` +//! 做钳制与跳转;本模块不直接操作编辑器。 + +use gpui_kit::assets::IconName; +use gpui_kit::component::{h_flex, v_flex, Icon}; +use gpui_kit::{ + div, px, rgba, Context, EventEmitter, FocusHandle, InteractiveElement as _, IntoElement, + KeyDownEvent, ParentElement as _, Render, Styled as _, Window, +}; + +use crate::theme::ThemeColors; + +/// 跳转到行弹窗对外事件。 +#[derive(Debug, Clone)] +pub enum GoToLineEvent { + /// 确认跳转,携带 1-based 行号。 + Confirm(u32), + /// 请求关闭弹窗。 + Close, +} + +/// 居中的单行输入小弹窗。 +pub struct GoToLineModal { + pub input: String, + pub focus_handle: FocusHandle, +} + +impl EventEmitter for GoToLineModal {} + +impl GoToLineModal { + pub fn new(cx: &mut Context) -> Self { + Self { + input: String::new(), + focus_handle: cx.focus_handle(), + } + } + + /// 复位到初始状态:清空输入。 + pub fn reset(&mut self, cx: &mut Context) { + self.input.clear(); + cx.notify(); + } + + /// 解析输入并确认:仅接受 `>= 1` 的数字,其余输入忽略。 + fn confirm(&self, cx: &mut Context) { + if let Ok(n) = self.input.trim().parse::() { + if n >= 1 { + cx.emit(GoToLineEvent::Confirm(n)); + } + } + } +} + +impl Render for GoToLineModal { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + // 请求聚焦以接收按键输入 + window.focus(&self.focus_handle, cx); + + // 全屏半透明遮罩:点击空白处关闭 + div() + .id("go-to-line-backdrop") + .track_focus(&self.focus_handle) + .absolute() + .inset_0() + .bg(rgba(0x00000088)) + .flex() + .items_center() + .justify_center() + .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { + let key = event.keystroke.key.as_str(); + match key { + "escape" => { + cx.emit(GoToLineEvent::Close); + } + "enter" => { + this.confirm(cx); + } + "backspace" => { + this.input.pop(); + cx.notify(); + } + _ => { + // 无修饰键时只接受 ASCII 数字,最多 6 位。 + if !event.keystroke.modifiers.control + && !event.keystroke.modifiers.alt + && !event.keystroke.modifiers.platform + && this.input.len() < 6 + { + let mut digit: Option = None; + if let Some(ch) = &event.keystroke.key_char { + let mut chars = ch.chars(); + if let (Some(c), None) = (chars.next(), chars.next()) { + if c.is_ascii_digit() { + digit = Some(c); + } + } + } else if key.chars().count() == 1 { + if let Some(c) = key.chars().next() { + if c.is_ascii_digit() { + digit = Some(c); + } + } + } + if let Some(c) = digit { + this.input.push(c); + cx.notify(); + } + } + } + } + })) + .on_mouse_down( + gpui_kit::MouseButton::Left, + cx.listener(|_this, _event, _window, cx| { + cx.emit(GoToLineEvent::Close); + }), + ) + .child( + v_flex() + .id("go-to-line-card") + .w(px(400.0)) + .bg(ThemeColors::surface()) + .border_1() + .border_color(ThemeColors::border()) + .rounded_lg() + .shadow_lg() + .overflow_hidden() + .on_mouse_down( + gpui_kit::MouseButton::Left, + cx.listener(|_this, _event, _window, cx| { + // 卡片内点击不冒泡到遮罩,避免误关闭 + cx.stop_propagation(); + }), + ) + .child( + // 标题行 + h_flex() + .h(px(36.0)) + .w_full() + .items_center() + .px_4() + .border_b_1() + .border_color(ThemeColors::border()) + .text_sm() + .text_color(ThemeColors::foreground()) + .child(crate::i18n::menu_text(cx, "goToLine.title").to_string()), + ) + .child( + // 输入行:行号图标 + 输入串/占位 + Enter 徽标 + h_flex() + .h(px(52.0)) + .w_full() + .items_center() + .gap_2p5() + .px_4() + .border_b_1() + .border_color(ThemeColors::border()) + .child( + Icon::new(IconName::Hash) + .size(px(16.0)) + .text_color(ThemeColors::primary()), + ) + .child( + h_flex() + .flex_1() + .items_center() + .gap_1() + .child( + div() + .text_sm() + .text_color(if self.input.is_empty() { + ThemeColors::subtle_foreground() + } else { + ThemeColors::foreground() + }) + .child(if self.input.is_empty() { + crate::i18n::menu_text(cx, "goToLine.placeholder") + .to_string() + } else { + self.input.clone() + }), + ) + .child( + // 静态光标条,提示可输入 + div().w(px(2.0)).h(px(16.0)).bg(ThemeColors::primary()), + ), + ) + .child(shortcut_badge("Enter")), + ) + .child( + // 底部提示行 + div() + .w_full() + .px_4() + .py_2() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child(crate::i18n::menu_text(cx, "goToLine.hint").to_string()), + ), + ) + } +} + +/// 快捷键/键位徽标。 +fn shortcut_badge(label: &str) -> impl IntoElement { + div() + .px_1p5() + .py(px(1.0)) + .rounded_sm() + .bg(ThemeColors::accent()) + .border_1() + .border_color(ThemeColors::border()) + .text_xs() + .text_color(ThemeColors::muted_foreground()) + .child(label.to_string()) +} diff --git a/linux/src/workbench/maven.rs b/linux/src/workbench/maven.rs new file mode 100644 index 000000000..84673fc3f --- /dev/null +++ b/linux/src/workbench/maven.rs @@ -0,0 +1,1712 @@ +//! 右侧 Maven 导航工具窗口:项目树 + 生命周期阶段 + 依赖 + 自定义 goal,对齐 Tauri +//! `windows/tauri/src/features/maven/` 的导航语义。 +//! +//! 本面板只负责导航展示与事件发射:点击阶段行 / 自定义 goal 执行按钮发射 +//! [`MavenEvent::RunGoal`],实际命令执行由外部经底部 Terminal 完成(`view.rs` +//! 拼出 `mvn -f "" `)。工程结构(`maven.scan`)与依赖加载 +//! (`maven.dependencyPlan` + `maven.dependencies`)经 [`CoreClient`] 在 +//! `cx.spawn` 里异步调用 core 命令;本地 pom 发现([`find_pom_files`])先行, +//! core 失败只落工程/依赖状态,不阻塞导航展示。 + +use std::collections::{HashMap, HashSet}; + +use gpui_kit::assets::IconName; +use gpui_kit::component::button::{Button, ButtonVariants as _}; +use gpui_kit::component::input::{Input, InputEvent, InputState}; +use gpui_kit::component::scroll::ScrollableElement as _; +use gpui_kit::component::{h_flex, v_flex, Disableable as _, Icon, Sizable as _}; +use gpui_kit::prelude::FluentBuilder as _; +use gpui_kit::{ + div, px, AppContext as _, ClickEvent, Context, Entity, EventEmitter, FontWeight, + InteractiveElement as _, IntoElement, ParentElement as _, Render, + StatefulInteractiveElement as _, Styled as _, Subscription, Window, +}; + +use crate::core::CoreClient; +use crate::theme::ThemeColors; + +/// 对齐 Tauri `MAVEN_LIFECYCLE_PHASES` 的 Maven 生命周期阶段。 +pub const MAVEN_LIFECYCLE_PHASES: [&str; 9] = [ + "clean", "validate", "compile", "test", "package", "verify", "install", "site", "deploy", +]; + +/// pom 扫描最大递归深度。 +const MAX_POM_SCAN_DEPTH: usize = 5; + +/// pom 扫描跳过的目录名:构建产物与常见工具目录。 +const POM_SCAN_SKIP_DIRS: [&str; 9] = [ + "target", + "node_modules", + ".git", + ".idea", + "dist", + "build", + "out", + ".vscode", + "vendor", +]; + +/// 递归扫描 root 下的 `pom.xml`,返回排序后的绝对路径列表,不新增依赖。 +pub fn find_pom_files(root: &str) -> Vec { + let mut out = Vec::new(); + let base = std::path::PathBuf::from(root); + let base = if base.is_absolute() { + base + } else { + std::env::current_dir() + .map(|cwd| cwd.join(&base)) + .unwrap_or(base) + }; + walk_pom_dir(&base, 0, &mut out); + out.sort(); + out +} + +fn walk_pom_dir(dir: &std::path::Path, depth: usize, out: &mut Vec) { + if depth > MAX_POM_SCAN_DEPTH { + return; + } + let Ok(entries) = std::fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + let name = entry.file_name().to_string_lossy().into_owned(); + if path.is_dir() { + if POM_SCAN_SKIP_DIRS.contains(&name.as_str()) { + continue; + } + walk_pom_dir(&path, depth + 1, out); + } else if name == "pom.xml" { + out.push(path.to_string_lossy().into_owned()); + } + } +} + +/// Lifecycle 组的展开键。 +fn lifecycle_key(owner: &str) -> String { + format!("{owner}:lifecycle") +} + +/// 依赖组的展开键。 +fn deps_key(owner: &str) -> String { + format!("{owner}:dependencies") +} + +/// 源码根目录组的展开键。 +fn source_roots_key(owner: &str) -> String { + format!("{owner}:source-roots") +} + +/// 模块节点的展开键与 owner 键:根项目为 `project`,子模块为 +/// `module:{relativePath}`(与 `maven.scan` 的模块路径约定一致)。 +fn owner_key(relative_path: &str) -> String { + if relative_path == "." || relative_path.is_empty() { + "project".to_string() + } else { + format!("module:{relative_path}") + } +} + +/// owner 键反推模块相对路径:根项目为 `.`。 +fn relative_for_owner(owner: &str) -> String { + owner.strip_prefix("module:").unwrap_or(".").to_string() +} + +/// 模块相对路径对应的 pom 绝对路径。 +fn pom_for_relative(root: &str, relative_path: &str) -> String { + let mut path = std::path::PathBuf::from(root); + if !relative_path.is_empty() && relative_path != "." { + path.push(relative_path); + } + path.push("pom.xml"); + path.to_string_lossy().into_owned() +} + +/// pom 行的显示路径:相对 root,根 pom 显示 `pom.xml`。 +fn display_pom_path(root: &str, pom: &str) -> String { + std::path::Path::new(pom) + .strip_prefix(root) + .map(|rel| { + let s = rel.to_string_lossy().into_owned(); + if s.is_empty() { + pom.to_string() + } else { + s + } + }) + .unwrap_or_else(|_| pom.to_string()) +} + +/// 传给 `maven.scan` 的 workspace 相对 pom 路径:core 只接受相对路径, +/// 绝对路径会被 core 侧忽略(扫描退化为无工程,不报错)。 +fn relative_pom_paths(root: &str, poms: &[String]) -> Vec { + poms.iter() + .map(|pom| { + std::path::Path::new(pom) + .strip_prefix(root) + .map(|rel| rel.to_string_lossy().replace('\\', "/")) + .unwrap_or_else(|_| pom.clone()) + }) + .collect() +} + +/// 对齐 Tauri `projectStatus` 的工程状态:`idle` / `loading` / `ready` / `failed`; +/// 依赖加载另有 `Cancelled`(用户主动取消,可重试)。 +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MavenStatus { + Idle, + Loading, + Ready, + Failed(String), + Cancelled, +} + +/// 依赖列表行的完整字段,对齐 core `maven.dependencies` 返回的 +/// `dependencies[]`(含嵌套 `children` 与 `resolution` 标记)。 +#[derive(Debug, Clone)] +pub struct MavenDep { + pub group: String, + pub artifact: String, + pub version: String, + pub dep_type: String, + pub classifier: Option, + pub scope: String, + /// `resolved` / `omittedConflict` / `omittedDuplicate`…… + pub resolution: String, + pub selected_version: Option, + pub children: Vec, +} + +/// 从 `maven.dependencies` 的返回体递归解析依赖树,缺失三元组直接丢弃。 +fn parse_dep(item: &serde_json::Value) -> Option { + let children = item + .get("children") + .and_then(|value| value.as_array()) + .map(|items| items.iter().filter_map(parse_dep).collect()) + .unwrap_or_default(); + Some(MavenDep { + group: item.get("groupId")?.as_str()?.to_string(), + artifact: item.get("artifactId")?.as_str()?.to_string(), + version: item + .get("version") + .and_then(|value| value.as_str()) + .unwrap_or_default() + .to_string(), + dep_type: item + .get("type") + .and_then(|value| value.as_str()) + .unwrap_or("jar") + .to_string(), + classifier: item + .get("classifier") + .and_then(|value| value.as_str()) + .filter(|s| !s.is_empty()) + .map(str::to_string), + scope: item + .get("scope") + .and_then(|value| value.as_str()) + .unwrap_or_default() + .to_string(), + resolution: item + .get("resolution") + .and_then(|value| value.as_str()) + .unwrap_or("resolved") + .to_string(), + selected_version: item + .get("selectedVersion") + .and_then(|value| value.as_str()) + .map(str::to_string), + children, + }) +} + +fn parse_deps(data: &serde_json::Value) -> Vec { + data.get("dependencies") + .and_then(|value| value.as_array()) + .map(|items| items.iter().filter_map(parse_dep).collect()) + .unwrap_or_default() +} + +/// 依赖副标题:`groupId:version:type[:classifier] [scope] [(标记)]`, +/// 与 Tauri `renderDependency` 的 subtitle 口径一致。 +fn dep_subtitle(dep: &MavenDep, cx: &gpui_kit::App) -> String { + let classifier = dep + .classifier + .as_deref() + .map(|c| format!(":{c}")) + .unwrap_or_default(); + let marker = if dep.resolution == "omittedConflict" { + let arrow = dep + .selected_version + .as_deref() + .map(|v| format!(" -> {v}")) + .unwrap_or_default(); + format!( + " ({}{arrow})", + crate::i18n::menu_text(cx, "maven.omittedConflict") + ) + } else if dep.resolution == "omittedDuplicate" { + format!( + " ({})", + crate::i18n::menu_text(cx, "maven.omittedDuplicate") + ) + } else { + String::new() + }; + format!( + "{}:{}:{}{} [{}]{marker}", + dep.group, dep.version, dep.dep_type, classifier, dep.scope + ) +} + +/// 扫描得到的模块节点:`maven.scan` 响应体的内存子集 +///(`relativePath` / `artifactId` / `packaging` / `sourceRoots` / `modules`)。 +#[derive(Debug, Clone, Default)] +pub struct MavenModuleNode { + pub relative_path: String, + pub artifact_id: String, + pub packaging: String, + pub source_roots: Vec<(String, String)>, + pub modules: Vec, +} + +/// 扫描得到的 profile:`profiles[]` 的 `id` 与默认激活态。 +#[derive(Debug, Clone)] +pub struct MavenProfileItem { + pub id: String, + pub active_by_default: bool, +} + +fn parse_module_node(value: &serde_json::Value) -> Option { + Some(MavenModuleNode { + relative_path: value + .get("relativePath") + .and_then(|v| v.as_str()) + .unwrap_or(".") + .to_string(), + artifact_id: value + .get("artifactId") + .and_then(|v| v.as_str())? + .to_string(), + packaging: value + .get("packaging") + .and_then(|v| v.as_str()) + .unwrap_or("jar") + .to_string(), + source_roots: value + .get("sourceRoots") + .and_then(|v| v.as_array()) + .map(|roots| { + roots + .iter() + .filter_map(|root| { + Some(( + root.get("path")?.as_str()?.to_string(), + root.get("kind") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(), + )) + }) + .collect() + }) + .unwrap_or_default(), + modules: value + .get("modules") + .and_then(|v| v.as_array()) + .map(|items| items.iter().filter_map(parse_module_node).collect()) + .unwrap_or_default(), + }) +} + +fn parse_profiles(value: &serde_json::Value) -> Vec { + value + .get("profiles") + .and_then(|v| v.as_array()) + .map(|items| { + items + .iter() + .filter_map(|item| { + Some(MavenProfileItem { + id: item.get("id")?.as_str()?.to_string(), + active_by_default: item + .get("isActiveByDefault") + .and_then(|v| v.as_bool()) + .unwrap_or(false), + }) + }) + .collect() + }) + .unwrap_or_default() +} + +/// Maven 导航面板派发的事件:执行只发射,由外部经底部 Maven 页执行; +/// 依赖行点击发射 `OpenFile`(打开模块 pom),设置按钮发射 `OpenSettings`。 +#[derive(Debug, Clone)] +pub enum MavenEvent { + RunGoal { + pom_path: String, + phase: String, + /// 展示用目标(`{phase} · {artifactId}`,对齐 Tauri 任务标题)。 + target: String, + profiles: Vec, + skip_tests: bool, + }, + OpenFile(String), + OpenSettings, + Close, +} + +/// 右侧 Maven 导航工具窗口:模块树 + 生命周期 + 依赖 + Profiles + 自定义 goal, +/// 对齐 Tauri `MavenPane` 的树语义(caret 与标签分离、子节点引导线、 +/// 单击选中、phase 双击运行)。 +pub struct MavenView { + pub root: String, + /// 本地 pom 兜底列表:core 无工程时沿用旧平铺展示。 + poms: Vec, + /// 扫描得到的反应堆根节点(`relativePath == "."`)。 + project: Option, + profiles: Vec, + selected_profiles: HashSet, + skip_tests: bool, + expanded: HashSet, + /// 选中的模块相对路径;`None` 为根项目。 + selected_module: Option, + selected_phase: String, + project_status: MavenStatus, + dependencies: HashMap>, + dep_status: HashMap, + dep_seq: HashMap, + dep_ops: HashMap, + custom_goal: String, + goal_input: Option>, + _goal_subscription: Option, + client: CoreClient, + scan_seq: u64, +} + +impl EventEmitter for MavenView {} + +impl MavenView { + pub fn new(root: String, cx: &mut Context) -> Self { + let mut view = Self { + root, + poms: Vec::new(), + project: None, + profiles: Vec::new(), + selected_profiles: HashSet::new(), + skip_tests: false, + expanded: HashSet::new(), + selected_module: None, + selected_phase: "compile".to_string(), + project_status: MavenStatus::Idle, + dependencies: HashMap::new(), + dep_status: HashMap::new(), + dep_seq: HashMap::new(), + dep_ops: HashMap::new(), + custom_goal: String::new(), + goal_input: None, + _goal_subscription: None, + client: CoreClient::new(), + scan_seq: 0, + }; + view.reload(cx); + view + } + + pub fn set_root(&mut self, root: String, cx: &mut Context) { + self.root = root; + self.project = None; + self.profiles.clear(); + self.selected_profiles.clear(); + self.selected_module = None; + self.dependencies.clear(); + self.dep_status.clear(); + self.reload(cx); + } + + pub fn refresh(&mut self, cx: &mut Context) { + self.project = None; + self.profiles.clear(); + self.selected_profiles.clear(); + self.dependencies.clear(); + self.dep_status.clear(); + self.reload(cx); + } + + pub fn has_projects(&self) -> bool { + self.project.is_some() || !self.poms.is_empty() + } + + /// 全部折叠(对齐 Tauri `collapseAll`)。 + fn collapse_all(&mut self, cx: &mut Context) { + self.expanded.clear(); + cx.notify(); + } + + /// 重扫 pom 列表(core 无工程时的兜底展示),默认展开全部。 + fn rescan(&mut self) { + self.poms = find_pom_files(&self.root); + self.expanded.clear(); + for pom in &self.poms { + self.expanded.insert(pom.clone()); + self.expanded.insert(lifecycle_key(pom)); + } + } + + /// 本地重扫 + core `maven.scan` 拿模块结构: + /// payload `{root, paths}`(相对 pom 路径)。成功解析模块树、 + /// Profiles 与默认选中(`isActiveByDefault`),初始展开根项目与 + /// Profiles(对齐 Tauri 初次展开);失败只落 `project_status`, + /// 导航照常展示本地 pom 兜底。 + fn reload(&mut self, cx: &mut Context) { + self.rescan(); + self.project_status = MavenStatus::Loading; + self.scan_seq += 1; + let seq = self.scan_seq; + cx.notify(); + + let client = self.client.clone(); + let root = self.root.clone(); + let paths = relative_pom_paths(&self.root, &self.poms); + cx.spawn(async move |this, cx| { + let payload = serde_json::json!({ "root": root, "paths": paths }); + let result = client + .execute::(&cx, "maven.scan", payload) + .await; + let _ = this.update(cx, |view, cx| { + if view.scan_seq != seq { + return; + } + match result { + Ok(data) => { + view.project = parse_module_node(&data); + view.profiles = parse_profiles(&data); + view.selected_profiles = view + .profiles + .iter() + .filter(|p| p.active_by_default) + .map(|p| p.id.clone()) + .collect(); + view.selected_module = None; + view.selected_phase = "compile".to_string(); + view.expanded.clear(); + view.expanded.insert("project".to_string()); + if !view.profiles.is_empty() { + view.expanded.insert("profiles".to_string()); + } + view.project_status = MavenStatus::Ready; + } + Err(err) => { + view.project = None; + view.profiles.clear(); + view.selected_profiles.clear(); + view.project_status = MavenStatus::Failed(err); + } + } + cx.notify(); + }); + }) + .detach(); + } + + fn toggle_node(&mut self, key: &str, cx: &mut Context) { + if !self.expanded.remove(key) { + self.expanded.insert(key.to_string()); + } + cx.notify(); + } + + /// 依赖组展开即加载、收起即保留缓存(对齐 Tauri 展开加载语义)。 + fn toggle_deps(&mut self, relative_path: &str, cx: &mut Context) { + let key = deps_key(&owner_key(relative_path)); + if !self.expanded.remove(&key) { + self.expanded.insert(key); + self.load_deps(relative_path, cx); + } else { + cx.notify(); + } + } + + /// 取消指定模块的依赖加载(序号递增丢弃迟到结果,对齐 Tauri 取消)。 + fn cancel_deps(&mut self, relative_path: &str, cx: &mut Context) { + let seq = self.dep_seq.get(relative_path).copied().unwrap_or(0) + 1; + self.dep_seq.insert(relative_path.to_string(), seq); + if let Some(op) = self.dep_ops.remove(relative_path) { + self.client.cancel(&op); + } + self.dep_status + .insert(relative_path.to_string(), MavenStatus::Cancelled); + cx.notify(); + } + + /// 选中 phase(单击语义,不执行)。 + fn select_phase(&mut self, relative_path: &str, phase: &str, cx: &mut Context) { + self.selected_module = if relative_path == "." { + None + } else { + Some(relative_path.to_string()) + }; + self.selected_phase = phase.to_string(); + cx.notify(); + } + + /// 发射执行事件:标题 `{phase} · {artifactId}`(对齐 Tauri 任务标题)。 + fn emit_run_goal(&mut self, relative_path: &str, phase: &str, cx: &mut Context) { + self.select_phase(relative_path, phase, cx); + let artifact = self + .find_module(relative_path) + .map(|m| m.artifact_id.clone()) + .or_else(|| self.project.as_ref().map(|p| p.artifact_id.clone())) + .unwrap_or_else(|| crate::i18n::menu_text(cx, "maven.project").to_string()); + let mut profiles: Vec = self.selected_profiles.iter().cloned().collect(); + profiles.sort(); + cx.emit(MavenEvent::RunGoal { + pom_path: pom_for_relative(&self.root, relative_path), + phase: phase.to_string(), + target: artifact, + profiles, + skip_tests: self.skip_tests, + }); + } + + /// 运行当前选中 scope(头部运行按钮,对齐 Tauri `runSelected`)。 + fn run_selected(&mut self, cx: &mut Context) { + if self.project.is_none() { + return; + } + let relative = self + .selected_module + .clone() + .unwrap_or_else(|| ".".to_string()); + let phase = self.selected_phase.clone(); + self.emit_run_goal(&relative, &phase, cx); + } + + /// 在模块树中按相对路径查找节点(含根)。 + fn find_module(&self, relative_path: &str) -> Option<&MavenModuleNode> { + if relative_path == "." || relative_path.is_empty() { + return self.project.as_ref(); + } + fn walk<'a>(node: &'a MavenModuleNode, rel: &str) -> Option<&'a MavenModuleNode> { + if node.relative_path == rel { + return Some(node); + } + node.modules.iter().find_map(|child| walk(child, rel)) + } + self.project + .as_ref() + .and_then(|root| walk(root, relative_path)) + } + + /// 先 `maven.dependencyPlan` 校验模块并拿出确定的离线调用计划 + /// (payload `{root, context: {version: 1, reactorPath: ".", profiles, skipTests}, module}`, + /// 返回 `MavenLaunchPlanResponse`),再 `maven.dependencies` + /// (payload `{modulePath, output}`,返回 `{modulePath, dependencies[]}`) + /// 递归解析依赖树。Linux 侧无 Maven 进程宿主,`output` 为空即返回空列表; + /// 真实树需在终端运行 `dependency:tree`,失败文案直接展示。 + /// 每个模块独立序号与 operationId,支持取消(对齐 Tauri 各模块独立加载)。 + fn load_deps(&mut self, relative_path: &str, cx: &mut Context) { + let seq = self.dep_seq.get(relative_path).copied().unwrap_or(0) + 1; + self.dep_seq.insert(relative_path.to_string(), seq); + self.dep_status + .insert(relative_path.to_string(), MavenStatus::Loading); + self.dependencies.remove(relative_path); + cx.notify(); + + let client = self.client.clone(); + let root = self.root.clone(); + let module = relative_path.to_string(); + let mut profiles: Vec = self.selected_profiles.iter().cloned().collect(); + profiles.sort(); + let skip_tests = self.skip_tests; + let op = uuid::Uuid::new_v4().to_string(); + self.dep_ops.insert(relative_path.to_string(), op.clone()); + cx.spawn(async move |this, cx| { + let plan = client + .execute_with_operation_id::( + &cx, + "maven.dependencyPlan", + serde_json::json!({ + "root": root, + "context": { + "version": 1, + "reactorPath": ".", + "profiles": profiles, + "skipTests": skip_tests, + }, + "module": module, + }), + Some(op), + ) + .await; + let outcome: Result, String> = match plan { + Ok(_) => match client + .execute::( + &cx, + "maven.dependencies", + serde_json::json!({ "modulePath": module, "output": "" }), + ) + .await + { + Ok(data) => Ok(parse_deps(&data)), + Err(err) => Err(err), + }, + Err(err) => Err(err), + }; + let _ = this.update(cx, |view, cx| { + if view.dep_seq.get(&module).copied().unwrap_or(0) != seq { + return; + } + view.dep_ops.remove(&module); + match outcome { + Ok(deps) => { + view.dependencies.insert(module.clone(), deps); + view.dep_status.insert(module.clone(), MavenStatus::Ready); + } + Err(err) => { + view.dep_status + .insert(module.clone(), MavenStatus::Failed(err)); + } + } + cx.notify(); + }); + }) + .detach(); + } + + /// 懒创建自定义 goal 输入框(参考 `settings_dialog.rs::ensure_input`)。 + fn ensure_goal_input( + slot: &mut Option>, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + if let Some(entity) = slot.clone() { + return entity; + } + let entity = cx.new(|cx| InputState::new(window, cx)); + *slot = Some(entity.clone()); + entity + } +} + +/// pom 所在目录相对 root 的模块路径:根 pom 为 `.`(兜底展示用)。 +fn relative_for_pom(root: &str, pom: &str) -> String { + let relative = std::path::Path::new(pom) + .parent() + .and_then(|dir| dir.strip_prefix(root).ok()) + .map(|rel| rel.to_string_lossy().replace('\\', "/")) + .unwrap_or_default(); + let trimmed = relative.trim_matches('/').to_string(); + if trimmed.is_empty() { + ".".to_string() + } else { + trimmed + } +} + +/// 子节点容器:左缩进 + 引导线,对齐 Tauri `ml-4 border-l pl-1`。 +fn child_container(children: Vec) -> gpui_kit::AnyElement { + div() + .w_full() + .ml(px(16.0)) + .pl(px(4.0)) + .border_l_1() + .border_color(ThemeColors::border()) + .children(children) + .into_any_element() +} + +impl MavenView { + /// 分发 caret/标签的折叠动作:普通节点切 `expanded`,依赖组附带加载。 + fn dispatch_toggle(&mut self, key: &str, cx: &mut Context) { + if let Some(owner) = key.strip_suffix(":dependencies") { + self.toggle_deps(&relative_for_owner(owner), cx); + } else { + self.toggle_node(key, cx); + } + } +} + +impl Render for MavenView { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + // 自定义 goal 输入框懒创建 + 一次性订阅 Change 事件,值同步到 custom_goal。 + let goal_entity = Self::ensure_goal_input(&mut self.goal_input, window, cx); + if self._goal_subscription.is_none() { + let entity = goal_entity.clone(); + self._goal_subscription = Some(cx.subscribe( + &goal_entity, + move |this: &mut Self, _, event: &InputEvent, cx| { + if matches!(event, InputEvent::Change) { + this.custom_goal = entity.read(cx).value().to_string(); + } + }, + )); + } + + // 快照后构建行,避免在闭包里同时借用 self 与 cx。 + let expanded = self.expanded.clone(); + let project = self.project.clone(); + let profiles = self.profiles.clone(); + let selected_profiles = self.selected_profiles.clone(); + let skip_tests = self.skip_tests; + let selected_module = self.selected_module.clone(); + let selected_phase = self.selected_phase.clone(); + let project_status = self.project_status.clone(); + let dep_status = self.dep_status.clone(); + let dependencies = self.dependencies.clone(); + let custom_goal = self.custom_goal.clone(); + let poms = self.poms.clone(); + + let title_text = match project.as_ref() { + Some(node) => format!( + "{} · {}", + crate::i18n::menu_text(cx, "maven.title"), + node.artifact_id + ), + None => crate::i18n::menu_text(cx, "maven.title").to_string(), + }; + let status_banner: Option<(bool, String)> = match &project_status { + MavenStatus::Loading => Some(( + false, + crate::i18n::menu_text(cx, "maven.scanning").to_string(), + )), + MavenStatus::Failed(err) => Some((true, err.clone())), + MavenStatus::Idle | MavenStatus::Ready | MavenStatus::Cancelled => None, + }; + let has_project = project.is_some(); + + // 当前选中 scope 的执行信息(头部运行按钮与自定义 goal 共用)。 + let scope_rel = selected_module.clone().unwrap_or_else(|| ".".to_string()); + let scope_pom = pom_for_relative(&self.root, &scope_rel); + let scope_target = project + .as_ref() + .and_then(|root| { + if scope_rel == "." { + Some(root.artifact_id.clone()) + } else { + find_module_in(root, &scope_rel).map(|m| m.artifact_id.clone()) + } + }) + .unwrap_or_else(|| display_pom_path(&self.root, &scope_pom)); + let scope_profiles: Vec = { + let mut ids: Vec = selected_profiles.iter().cloned().collect(); + ids.sort(); + ids + }; + + let mut body = Vec::new(); + if let Some(root_node) = project.clone() { + if !profiles.is_empty() { + body.push(self.render_profiles_node(&profiles, &selected_profiles, &expanded, cx)); + } + body.push(self.render_module_node( + &root_node, + None, + &expanded, + &selected_module, + &selected_phase, + &dep_status, + &dependencies, + cx, + )); + } else if !poms.is_empty() { + // core 无工程时的 pom 兜底:按 pom 所在目录合成 owner 行。 + for pom in &poms { + let rel = relative_for_pom(&self.root, pom); + let owner = owner_key(&rel); + let selected = if rel == "." { + selected_module.is_none() + } else { + selected_module.as_deref() == Some(rel.as_str()) + }; + body.push(self.render_owner_row( + format!("legacy-{owner}"), + OwnerToggle::Key(owner.clone()), + IconName::Package, + display_pom_path(&self.root, pom), + None, + selected, + OwnerSelect::Module(if rel == "." { None } else { Some(rel.clone()) }), + cx, + )); + if expanded.contains(&owner) { + let mut children = Vec::new(); + children.push(self.render_lifecycle_node( + &owner, + &rel, + &expanded, + &selected_module, + &selected_phase, + cx, + )); + children.push(self.render_deps_node( + &owner, + &rel, + &self.root, + &expanded, + &dep_status, + &dependencies, + cx, + )); + body.push(child_container(children)); + } + } + } + + let body_element = match (&project, &project_status) { + (None, MavenStatus::Failed(err)) if poms.is_empty() => v_flex() + .flex_1() + .w_full() + .items_center() + .justify_center() + .gap_2() + .p_4() + .child( + Icon::new(IconName::TriangleAlert) + .size(px(28.0)) + .text_color(ThemeColors::destructive()), + ) + .child( + div() + .text_xs() + .font_weight(FontWeight::BOLD) + .text_color(ThemeColors::text_primary()) + .child(crate::i18n::menu_text(cx, "maven.loadFailed")), + ) + .child( + div() + .w_full() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(err.clone()), + ) + .child( + Button::new("maven-retry") + .small() + .primary() + .label(crate::i18n::menu_text(cx, "ui.retry")) + .on_click(cx.listener(|this, _event, _window, cx| { + this.refresh(cx); + })), + ) + .into_any_element(), + (None, _) if body.is_empty() => div() + .flex_1() + .w_full() + .flex() + .items_center() + .justify_center() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(crate::i18n::menu_text( + cx, + match project_status { + MavenStatus::Loading => "maven.scanning", + _ => "maven.notDetected", + }, + )) + .into_any_element(), + _ => div() + .flex_1() + .w_full() + .min_h_0() + .overflow_y_scrollbar() + .px_2() + .py_1() + .children(body) + .into_any_element(), + }; + + let goal_entity_for_run = goal_entity.clone(); + let run_scope = (scope_pom.clone(), scope_target.clone()); + let run_profiles = scope_profiles.clone(); + v_flex() + .size_full() + .bg(ThemeColors::bg_sidebar()) + .border_l_1() + .border_color(ThemeColors::border()) + .child( + h_flex() + .h(px(32.0)) + .w_full() + .bg(ThemeColors::bg_sidebar()) + .border_b_1() + .border_color(ThemeColors::border()) + .items_center() + .justify_between() + .px_3() + .child( + div() + .flex_1() + .truncate() + .text_xs() + .font_weight(FontWeight::BOLD) + .text_color(ThemeColors::text_muted()) + .child(title_text), + ) + .child( + Button::new("maven-close") + .small() + .ghost() + .icon(IconName::Close) + .tooltip(crate::i18n::menu_text(cx, "ui.close")) + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(MavenEvent::Close); + })), + ), + ) + .child( + // 工具栏:运行选中 / 执行目标文本 / reload / 跳过测试 / + // 全部折叠 / 设置,对齐 Tauri 第二行。 + h_flex() + .h(px(32.0)) + .w_full() + .flex_shrink_0() + .items_center() + .gap_1() + .px_2() + .border_b_1() + .border_color(ThemeColors::border()) + .child( + Button::new("maven-run-selected") + .small() + .ghost() + .icon(IconName::Play) + .tooltip(crate::i18n::menu_text(cx, "maven.runSelected")) + .disabled(!has_project) + .on_click(cx.listener(|this, _event, _window, cx| { + this.run_selected(cx); + })), + ) + .child( + Button::new("maven-exec-goal") + .small() + .ghost() + .icon(IconName::Terminal) + .tooltip(crate::i18n::menu_text(cx, "maven.executeGoal")) + .disabled(custom_goal.trim().is_empty() || run_scope.0.is_empty()) + .on_click(cx.listener(move |this, _event, _window, cx| { + let goal = goal_entity_for_run.read(cx).value().to_string(); + let goal = goal.trim().to_string(); + if goal.is_empty() { + return; + } + this.emit_text_goal( + &run_scope.0, + &goal, + &run_scope.1, + &run_profiles, + cx, + ); + })), + ) + .child( + Button::new("maven-reload") + .small() + .ghost() + .icon(IconName::RotateCw) + .tooltip(crate::i18n::menu_text(cx, "maven.reloadProjects")) + .on_click(cx.listener(|this, _event, _window, cx| { + this.refresh(cx); + })), + ) + .child( + Button::new("maven-skip-tests") + .small() + .when(skip_tests, |b| b.primary()) + .when(!skip_tests, |b| b.ghost()) + .icon(IconName::Check) + .tooltip(crate::i18n::menu_text(cx, "maven.skipTests")) + .on_click(cx.listener(|this, _event, _window, cx| { + this.skip_tests = !this.skip_tests; + cx.notify(); + })), + ) + .child( + Button::new("maven-collapse-all") + .small() + .ghost() + .icon(IconName::FoldVertical) + .tooltip(crate::i18n::menu_text(cx, "maven.collapseAll")) + .on_click(cx.listener(|this, _event, _window, cx| { + this.collapse_all(cx); + })), + ) + .child( + Button::new("maven-settings") + .small() + .ghost() + .icon(IconName::Settings) + .tooltip(crate::i18n::menu_text(cx, "maven.settings")) + .disabled(!has_project) + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(MavenEvent::OpenSettings); + })), + ), + ) + .when_some(status_banner, |this, (is_error, text)| { + this.child( + div() + .w_full() + .px_3() + .py_1p5() + .text_xs() + .border_b_1() + .border_color(ThemeColors::border()) + .text_color(if is_error { + ThemeColors::accent_red() + } else { + ThemeColors::text_muted() + }) + .child(text), + ) + }) + .child(body_element) + .child( + h_flex() + .w_full() + .items_center() + .gap_1p5() + .p_2() + .border_t_1() + .border_color(ThemeColors::border()) + .child( + div() + .flex_1() + .child(Input::new(&goal_entity).cleanable(true)), + ) + .child( + Button::new("maven-run-goal") + .small() + .primary() + .icon(IconName::Play) + .tooltip(crate::i18n::menu_text(cx, "maven.executeGoal")) + .disabled(custom_goal.trim().is_empty() || scope_pom.is_empty()) + .on_click(cx.listener(move |this, _event, _window, cx| { + let goal = this.custom_goal.trim().to_string(); + if goal.is_empty() { + return; + } + this.emit_text_goal( + &scope_pom, + &goal, + &scope_target, + &scope_profiles, + cx, + ); + })), + ), + ) + } +} + +/// owner 行标签点击行为:选中模块,或仅折叠(依赖组等由 caret 覆盖时)。 +#[derive(Clone)] +enum OwnerSelect { + Module(Option), + Toggle, +} + +/// owner 行折叠行为:普通节点切展开态,依赖组附带加载。 +#[derive(Clone)] +enum OwnerToggle { + Key(String), + Deps(String), +} + +/// 在模块树中按相对路径查找节点(含根)。 +fn find_module_in<'a>( + root: &'a MavenModuleNode, + relative_path: &str, +) -> Option<&'a MavenModuleNode> { + if relative_path == "." || relative_path.is_empty() { + return Some(root); + } + fn walk<'a>(node: &'a MavenModuleNode, rel: &str) -> Option<&'a MavenModuleNode> { + if node.relative_path == rel { + return Some(node); + } + node.modules.iter().find_map(|child| walk(child, rel)) + } + walk(root, relative_path) +} + +impl MavenView { + /// 树父节点行:caret 与标签分离,选中高亮,对齐 Tauri `TreeNode` 头部。 + #[allow(clippy::too_many_arguments)] + fn render_owner_row( + &self, + id: String, + toggle: OwnerToggle, + icon: IconName, + title: String, + subtitle: Option, + selected: bool, + select: OwnerSelect, + cx: &mut Context, + ) -> gpui_kit::AnyElement { + let caret_key = match &toggle { + OwnerToggle::Key(key) => key.clone(), + OwnerToggle::Deps(owner) => format!("{owner}:dependencies"), + }; + let expanded = self.expanded.contains(&caret_key); + let chevron = if expanded { + IconName::ChevronDown + } else { + IconName::ChevronRight + }; + let label_base_id = format!("{id}-label"); + let label = h_flex() + .id(label_base_id) + .flex_1() + .min_w_0() + .items_center() + .gap_1p5() + .py_1() + .pr_2() + .cursor_pointer() + .child( + Icon::new(icon) + .size(px(14.0)) + .text_color(ThemeColors::text_primary()), + ) + .child( + div() + .truncate() + .text_xs() + .text_color(ThemeColors::text_primary()) + .child(title), + ) + .when_some(subtitle, |el, text| { + el.child( + div() + .truncate() + .font_family("monospace") + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(text), + ) + }); + let label = match select { + OwnerSelect::Module(rel) => { + label.on_click(cx.listener(move |this, _event, _window, cx| { + this.selected_module = rel.clone(); + cx.notify(); + })) + } + OwnerSelect::Toggle => { + let toggle = toggle.clone(); + label.on_click(cx.listener(move |this, _event, _window, cx| { + let key = match &toggle { + OwnerToggle::Key(key) => key.clone(), + OwnerToggle::Deps(owner) => format!("{owner}:dependencies"), + }; + this.dispatch_toggle(&key, cx); + })) + } + }; + h_flex() + .id(id.clone()) + .min_h(px(28.0)) + .w_full() + .items_center() + .rounded_sm() + .when(selected, |row| row.bg(ThemeColors::subtle_selection())) + .when(!selected, |row| { + row.hover(|h| h.bg(ThemeColors::bg_tab_hover())) + }) + .child( + div() + .id(format!("{id}-caret")) + .flex_shrink_0() + .size(px(24.0)) + .flex() + .items_center() + .justify_center() + .cursor_pointer() + .child( + Icon::new(chevron) + .size(px(12.0)) + .text_color(ThemeColors::text_muted()), + ) + .on_click(cx.listener(move |this, _event, _window, cx| { + let key = match &toggle { + OwnerToggle::Key(key) => key.clone(), + OwnerToggle::Deps(owner) => format!("{owner}:dependencies"), + }; + this.dispatch_toggle(&key, cx); + })), + ) + .child(label) + .into_any_element() + } + + /// Profiles 节点:勾选进 `launchPlan` 上下文。 + fn render_profiles_node( + &self, + profiles: &[MavenProfileItem], + selected: &HashSet, + expanded: &HashSet, + cx: &mut Context, + ) -> gpui_kit::AnyElement { + let mut out = vec![self.render_owner_row( + "maven-profiles".to_string(), + OwnerToggle::Key("profiles".to_string()), + IconName::Folder, + crate::i18n::menu_text(cx, "maven.profiles").to_string(), + None, + false, + OwnerSelect::Toggle, + cx, + )]; + if expanded.contains("profiles") { + let mut children = Vec::new(); + for profile in profiles { + let checked = selected.contains(&profile.id); + let id = profile.id.clone(); + children.push( + h_flex() + .id(format!("maven-profile-{id}")) + .h(px(28.0)) + .w_full() + .items_center() + .gap_2() + .px_2() + .rounded_sm() + .cursor_pointer() + .text_xs() + .text_color(ThemeColors::text_primary()) + .hover(|h| h.bg(ThemeColors::bg_tab_hover())) + .child( + div() + .flex_shrink_0() + .size(px(14.0)) + .flex() + .items_center() + .justify_center() + .rounded_sm() + .border_1() + .border_color(ThemeColors::border()) + .when(checked, |el| { + el.bg(ThemeColors::accent_blue()) + .border_color(ThemeColors::accent_blue()) + }) + .child(Icon::new(IconName::Check).size(px(10.0)).text_color( + if checked { + ThemeColors::foreground() + } else { + ThemeColors::background() + }, + )), + ) + .child(div().flex_1().truncate().child(profile.id.clone())) + .on_click(cx.listener(move |this, _event, _window, cx| { + if !this.selected_profiles.remove(&id) { + this.selected_profiles.insert(id.clone()); + } + cx.notify(); + })) + .into_any_element(), + ); + } + out.push(child_container(children)); + } + div().w_full().children(out).into_any_element() + } + + /// 模块节点(含根项目):sourceRoots + lifecycle + dependencies + 嵌套模块。 + #[allow(clippy::too_many_arguments)] + fn render_module_node( + &self, + node: &MavenModuleNode, + _parent: Option, + expanded: &HashSet, + selected_module: &Option, + selected_phase: &str, + dep_status: &HashMap, + dependencies: &HashMap>, + cx: &mut Context, + ) -> gpui_kit::AnyElement { + let owner = owner_key(&node.relative_path); + let selected = if node.relative_path == "." { + selected_module.is_none() + } else { + selected_module.as_deref() == Some(node.relative_path.as_str()) + }; + let select_rel = if node.relative_path == "." { + None + } else { + Some(node.relative_path.clone()) + }; + let mut out = vec![self.render_owner_row( + format!("maven-{owner}"), + OwnerToggle::Key(owner.clone()), + IconName::Package, + node.artifact_id.clone(), + Some(node.packaging.clone()), + selected, + OwnerSelect::Module(select_rel), + cx, + )]; + if expanded.contains(&owner) { + let mut children = Vec::new(); + if !node.source_roots.is_empty() { + let key = source_roots_key(&owner); + children.push(self.render_owner_row( + format!("maven-{key}"), + OwnerToggle::Key(key.clone()), + IconName::Folder, + crate::i18n::menu_text(cx, "maven.sourceRoots").to_string(), + None, + false, + OwnerSelect::Toggle, + cx, + )); + if expanded.contains(&key) { + let mut root_rows = Vec::new(); + for (path, kind) in &node.source_roots { + root_rows.push( + h_flex() + .w_full() + .items_center() + .gap_1p5() + .h(px(24.0)) + .px_2() + .text_xs() + .child( + Icon::new(IconName::FileText) + .size(px(12.0)) + .text_color(ThemeColors::text_muted()), + ) + .child(div().flex_1().truncate().child(path.clone())) + .when(!kind.is_empty(), |el| { + el.child( + div() + .font_family("monospace") + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(kind.clone()), + ) + }) + .into_any_element(), + ); + } + children.push(child_container(root_rows)); + } + } + children.push(self.render_lifecycle_node( + &owner, + &node.relative_path, + expanded, + selected_module, + selected_phase, + cx, + )); + children.push(self.render_deps_node( + &owner, + &node.relative_path, + &self.root, + expanded, + dep_status, + dependencies, + cx, + )); + for child in &node.modules { + children.push(self.render_module_node( + child, + Some(owner.clone()), + expanded, + selected_module, + selected_phase, + dep_status, + dependencies, + cx, + )); + } + out.push(child_container(children)); + } + div().w_full().children(out).into_any_element() + } + + /// Lifecycle 节点:phase 单击选中、双击运行(对齐 Tauri 单击/双击语义)。 + fn render_lifecycle_node( + &self, + owner: &str, + relative_path: &str, + expanded: &HashSet, + selected_module: &Option, + selected_phase: &str, + cx: &mut Context, + ) -> gpui_kit::AnyElement { + let key = lifecycle_key(owner); + let mut out = vec![self.render_owner_row( + format!("maven-{key}"), + OwnerToggle::Key(key.clone()), + IconName::Cog, + crate::i18n::menu_text(cx, "maven.lifecycle").to_string(), + None, + false, + OwnerSelect::Toggle, + cx, + )]; + if expanded.contains(&key) { + let mut rows = Vec::new(); + for phase in MAVEN_LIFECYCLE_PHASES { + let is_selected = selected_phase == phase + && (if relative_path == "." { + selected_module.is_none() + } else { + selected_module.as_deref() == Some(relative_path) + }); + let rel = relative_path.to_string(); + let phase_owned = phase.to_string(); + rows.push( + h_flex() + .id(format!("maven-{owner}-phase-{phase}")) + .h(px(28.0)) + .w_full() + .items_center() + .gap_1p5() + .px_2() + .rounded_sm() + .cursor_pointer() + .text_xs() + .when(is_selected, |row| { + row.bg(ThemeColors::subtle_selection()) + .text_color(ThemeColors::text_primary()) + }) + .when(!is_selected, |row| { + row.text_color(ThemeColors::text_primary()) + .hover(|h| h.bg(ThemeColors::bg_tab_hover())) + }) + .child(Icon::new(IconName::Play).size(px(12.0)).text_color( + if is_selected { + ThemeColors::accent_green() + } else { + ThemeColors::text_muted() + }, + )) + .child(div().flex_1().truncate().child(phase)) + .on_click(cx.listener(move |this, event: &ClickEvent, _window, cx| { + let double = matches!( + event, + ClickEvent::Mouse(m) if m.down.click_count >= 2 + ); + if double { + this.emit_run_goal(&rel, &phase_owned, cx); + } else { + this.select_phase(&rel, &phase_owned, cx); + } + })) + .into_any_element(), + ); + } + out.push(child_container(rows)); + } + div().w_full().children(out).into_any_element() + } + + /// 依赖节点:嵌套树 + Tauri 同式副标题 + 加载/失败重试/取消/空态。 + #[allow(clippy::too_many_arguments)] + fn render_deps_node( + &self, + owner: &str, + relative_path: &str, + root: &str, + expanded: &HashSet, + dep_status: &HashMap, + dependencies: &HashMap>, + cx: &mut Context, + ) -> gpui_kit::AnyElement { + let key = deps_key(owner); + let mut out = vec![self.render_owner_row( + format!("maven-{key}"), + OwnerToggle::Deps(owner.to_string()), + IconName::Package, + crate::i18n::menu_text(cx, "maven.dependencies").to_string(), + None, + false, + OwnerSelect::Toggle, + cx, + )]; + if expanded.contains(&key) { + let status = dep_status + .get(relative_path) + .cloned() + .unwrap_or(MavenStatus::Idle); + let pom = pom_for_relative(root, relative_path); + let rel = relative_path.to_string(); + let content = match &status { + MavenStatus::Loading => div() + .w_full() + .h(px(32.0)) + .flex() + .items_center() + .gap_2() + .px_2() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child( + div() + .flex_1() + .truncate() + .child(crate::i18n::menu_text(cx, "maven.dependencyLoading")), + ) + .child( + Button::new(format!("maven-deps-cancel-{owner}")) + .small() + .ghost() + .label(crate::i18n::menu_text(cx, "ui.cancel")) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.cancel_deps(&rel, cx); + })), + ) + .into_any_element(), + MavenStatus::Failed(err) => v_flex() + .w_full() + .gap_1() + .px_2() + .py_1p5() + .child( + div() + .w_full() + .text_xs() + .text_color(ThemeColors::accent_red()) + .child(err.clone()), + ) + .child( + Button::new(format!("maven-deps-retry-{owner}")) + .small() + .ghost() + .label(crate::i18n::menu_text(cx, "ui.retry")) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.load_deps(&rel, cx); + })), + ) + .into_any_element(), + MavenStatus::Cancelled => div() + .w_full() + .flex() + .items_center() + .gap_2() + .px_2() + .py_1() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child( + div() + .flex_1() + .truncate() + .child(crate::i18n::menu_text(cx, "maven.dependencyCancelled")), + ) + .child( + Button::new(format!("maven-deps-retry-{owner}")) + .small() + .ghost() + .label(crate::i18n::menu_text(cx, "ui.retry")) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.load_deps(&rel, cx); + })), + ) + .into_any_element(), + MavenStatus::Idle | MavenStatus::Ready => match dependencies.get(relative_path) { + Some(deps) if !deps.is_empty() => div() + .w_full() + .children(Self::render_dep_rows(deps, &pom, owner, cx)) + .into_any_element(), + _ => div() + .w_full() + .px_2() + .py_1() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(crate::i18n::menu_text(cx, "maven.noDependencies")) + .into_any_element(), + }, + }; + out.push(child_container(vec![content])); + } + div().w_full().children(out).into_any_element() + } + + /// 依赖行递归渲染:标题 artifactId + Tauri 同式副标题;omitted 警告色; + /// 点击打开所属模块 pom(对齐 Tauri 点击开 pom)。 + fn render_dep_rows( + deps: &[MavenDep], + pom: &str, + owner: &str, + cx: &mut Context, + ) -> Vec { + deps.iter() + .enumerate() + .map(|(index, dep)| { + let omitted = dep.resolution != "resolved"; + let pom_owned = pom.to_string(); + let mut out = vec![h_flex() + .id(format!("maven-{owner}-dep-{index}-{}", dep.artifact)) + .min_h(px(28.0)) + .w_full() + .items_center() + .gap_1p5() + .px_2() + .rounded_sm() + .cursor_pointer() + .text_xs() + .text_color(ThemeColors::text_primary()) + .hover(|h| h.bg(ThemeColors::bg_tab_hover())) + .child( + Icon::new(IconName::Package) + .size(px(12.0)) + .text_color(if omitted { + ThemeColors::warning() + } else { + ThemeColors::text_primary() + }), + ) + .child(div().truncate().child(dep.artifact.clone())) + .child( + div() + .flex_1() + .truncate() + .font_family("monospace") + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(dep_subtitle(dep, cx)), + ) + .on_click(cx.listener(move |_this, _event, _window, cx| { + cx.emit(MavenEvent::OpenFile(pom_owned.clone())); + })) + .into_any_element()]; + if !dep.children.is_empty() { + out.push(child_container(Self::render_dep_rows( + &dep.children, + pom, + owner, + cx, + ))); + } + div().w_full().children(out).into_any_element() + }) + .collect() + } + + /// 自定义 goal 文本执行:标题 `{goal} · {target}`(对齐 Tauri 任务标题)。 + fn emit_text_goal( + &mut self, + pom_path: &str, + goal: &str, + target: &str, + profiles: &[String], + cx: &mut Context, + ) { + cx.emit(MavenEvent::RunGoal { + pom_path: pom_path.to_string(), + phase: goal.to_string(), + target: target.to_string(), + profiles: profiles.to_vec(), + skip_tests: self.skip_tests, + }); + } +} diff --git a/linux/src/workbench/mod.rs b/linux/src/workbench/mod.rs new file mode 100644 index 000000000..9d870daef --- /dev/null +++ b/linux/src/workbench/mod.rs @@ -0,0 +1,63 @@ +pub mod activity_rail; +pub mod bottom_panel; +pub mod branch_manager; +pub mod command_palette; +pub mod editor; +pub mod extensions_panel; +pub mod file_icon; +pub mod go_to_line; +pub mod maven; +pub mod notifications; +pub mod panes; +pub mod project_dialog; +pub mod quick_open; +pub mod run; +pub mod search_everywhere; +pub mod settings_dialog; +pub mod sidebar; +pub mod status_bar; +pub mod tab_menu; +pub mod terminal; +pub mod toolbar; +pub mod view; +pub mod welcome_screen; + +#[allow(unused_imports)] +pub use activity_rail::{ + ActivityRailEvent, ActivityRailView, PluginActivityRailView, PluginRailEvent, +}; +#[allow(unused_imports)] +pub use bottom_panel::BottomPanelView; +#[allow(unused_imports)] +pub use branch_manager::{BranchManagerEvent, BranchManagerView, BranchTab}; +#[allow(unused_imports)] +pub use command_palette::{CommandPaletteEvent, CommandPaletteModal}; +#[allow(unused_imports)] +pub use editor::EditorView; +#[allow(unused_imports)] +pub use extensions_panel::{ExtensionsEvent, ExtensionsView}; +#[allow(unused_imports)] +pub use file_icon::{file_image, folder_image}; +#[allow(unused_imports)] +pub use go_to_line::{GoToLineEvent, GoToLineModal}; +#[allow(unused_imports)] +pub use maven::{MavenEvent, MavenView}; +#[allow(unused_imports)] +pub use notifications::{NotificationsEvent, NotificationsView}; +#[allow(unused_imports)] +pub use project_dialog::{ProjectDialog, ProjectDialogEvent, ProjectDialogMode}; +#[allow(unused_imports)] +pub use quick_open::{QuickOpenEvent, QuickOpenModal}; +#[allow(unused_imports)] +pub use search_everywhere::{SearchEverywhereEvent, SearchEverywhereModal, SearchScope}; +#[allow(unused_imports)] +pub use settings_dialog::{SettingsCategory, SettingsDialog, SettingsEvent}; +#[allow(unused_imports)] +pub use sidebar::{SidebarEvent, SidebarTab, SidebarView}; +#[allow(unused_imports)] +pub use status_bar::{StatusBarEvent, StatusBarView}; +#[allow(unused_imports)] +pub use toolbar::{ToolbarEvent, ToolbarView}; +pub use view::WorkbenchView; +#[allow(unused_imports)] +pub use welcome_screen::{WelcomeEvent, WelcomeScreenView}; diff --git a/linux/src/workbench/notifications.rs b/linux/src/workbench/notifications.rs new file mode 100644 index 000000000..6f9d41047 --- /dev/null +++ b/linux/src/workbench/notifications.rs @@ -0,0 +1,566 @@ +//! 右侧通知工具窗口:对齐 Tauri `notifications-tool-window.tsx` 的展示语义。 +//! +//! 可见性由宿主持有(对齐 `right-tool-window-actions.ts` 的 toggle 语义: +//! 同视图再点关闭、面板 X 关闭、隐藏时保活不卸载),本视图不保存可见标志, +//! 因此隐藏后重显不会丢失列表与搜索态。通知源为诊断快照( +//! [`NotificationsView::set_diagnostics`] 全量替换)与手动系统事件 +//! ([`NotificationsView::push`] 追加),未读数口径与 Tauri 一致 +//! (`success` 类型不计入未读)。 + +use gpui_kit::assets::IconName; +use gpui_kit::component::button::{Button, ButtonVariants as _}; +use gpui_kit::component::input::{Input, InputState}; +use gpui_kit::component::menu::DropdownMenu as _; +use gpui_kit::component::scroll::ScrollableElement as _; +use gpui_kit::component::{h_flex, v_flex, Icon, Sizable as _}; +use gpui_kit::prelude::FluentBuilder as _; +use gpui_kit::{ + div, px, AppContext as _, Context, Entity, EventEmitter, FontWeight, InteractiveElement as _, + IntoElement, ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Window, +}; + +use crate::theme::ThemeColors; + +/// 通知面板派发的事件:`Close` 由头部 X 发出,宿主据此隐藏右侧栏(保活); +/// `OpenFile` 由诊断类行点击发出,载荷为工作区相对路径与从 1 起的行号。 +#[derive(Debug, Clone)] +pub enum NotificationsEvent { + Close, + OpenFile(String, u32), +} + +/// 通知类型,对齐 Tauri `NotificationEntry["type"]`;`Diagnostic` 为 Linux 侧 +/// 由诊断快照投递的定位类通知,点击时额外发出 `OpenFile`,筛选时归入错误桶。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NotificationKind { + Info, + Success, + // `Warning` 暂无生产者(诊断后端接入后由 `set_diagnostics` 按级别构造)。 + #[allow(dead_code)] + Warning, + Error, + Diagnostic, +} + +/// 类型筛选:对齐 Tauri 通知筛选菜单(全部/信息/成功/警告/错误)。 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum NotificationFilter { + #[default] + All, + Info, + Success, + Warning, + Error, +} + +impl NotificationFilter { + const ALL: [NotificationFilter; 5] = [ + NotificationFilter::All, + NotificationFilter::Info, + NotificationFilter::Success, + NotificationFilter::Warning, + NotificationFilter::Error, + ]; + + fn matches(self, kind: NotificationKind) -> bool { + match self { + NotificationFilter::All => true, + NotificationFilter::Info => kind == NotificationKind::Info, + NotificationFilter::Success => kind == NotificationKind::Success, + NotificationFilter::Warning => kind == NotificationKind::Warning, + // 诊断类通知按错误展示,归入错误桶。 + NotificationFilter::Error => { + kind == NotificationKind::Error || kind == NotificationKind::Diagnostic + } + } + } + + fn label_key(self) -> &'static str { + match self { + NotificationFilter::All => "notifications.filterAll", + NotificationFilter::Info => "notifications.filterInfo", + NotificationFilter::Success => "notifications.filterSuccess", + NotificationFilter::Warning => "notifications.filterWarnings", + NotificationFilter::Error => "notifications.filterErrors", + } + } + + fn as_str(self) -> &'static str { + match self { + NotificationFilter::All => "all", + NotificationFilter::Info => "info", + NotificationFilter::Success => "success", + NotificationFilter::Warning => "warning", + NotificationFilter::Error => "error", + } + } + + fn from_str(id: &str) -> Option { + match id { + "all" => Some(NotificationFilter::All), + "info" => Some(NotificationFilter::Info), + "success" => Some(NotificationFilter::Success), + "warning" => Some(NotificationFilter::Warning), + "error" => Some(NotificationFilter::Error), + _ => None, + } + } +} + +/// 单条通知:诊断类通过 `path` 携带定位信息,非诊断类为 `None`。 +#[derive(Debug, Clone)] +pub struct NotificationItem { + id: u64, + title: String, + body: String, + kind: NotificationKind, + read: bool, + path: Option, + line: u32, +} + +/// 右侧通知工具窗口:内部持有通知列表本地态。 +pub struct NotificationsView { + items: Vec, + next_id: u64, + search: Option>, + filter: NotificationFilter, + /// 展开详情的通知 id(对齐 Tauri 点击行打开详情)。 + selected: Option, +} + +impl EventEmitter for NotificationsView {} + +impl NotificationsView { + /// 构造空列表(对齐 Tauri:无通知时展示空态,不预置文案)。 + pub fn new(_cx: &mut Context) -> Self { + Self { + items: Vec::new(), + next_id: 0, + search: None, + filter: NotificationFilter::All, + selected: None, + } + } + + /// 宿主投递手动系统事件:追加一条未读 `Info` 通知。 + pub fn push( + &mut self, + title: impl Into, + body: impl Into, + cx: &mut Context, + ) { + self.items.push(NotificationItem { + id: self.next_id, + title: title.into(), + body: body.into(), + kind: NotificationKind::Info, + read: false, + path: None, + line: 1, + }); + self.next_id += 1; + cx.notify(); + } + + /// 全量替换诊断类通知(诊断快照语义):保留非诊断项,诊断项按新快照 + /// 重建为未读;`line` 为从 1 起的行号,与跨平台契约一致。 + /// + /// 当前尚无诊断生产者(底部 Diagnostics 面板仍为占位),保留给诊断后端 + /// 接入后调用。 + #[allow(dead_code)] + pub fn set_diagnostics( + &mut self, + diagnostics: Vec<(String, u32, String)>, + cx: &mut Context, + ) { + self.items + .retain(|item| item.kind != NotificationKind::Diagnostic); + for (path, line, message) in diagnostics { + let body = format!("{}:{}", path, line); + self.items.push(NotificationItem { + id: self.next_id, + title: message, + body, + kind: NotificationKind::Diagnostic, + read: false, + path: Some(path), + line, + }); + self.next_id += 1; + } + cx.notify(); + } + + /// 未读数:`Success` 类型不计入,与 Tauri 角标口径一致。 + pub fn unread_count(&self) -> usize { + self.items + .iter() + .filter(|item| !item.read && item.kind != NotificationKind::Success) + .count() + } + + /// 全部标为已读(宿主在打开面板时调用,对齐 Tauri 打开即已读)。 + pub fn mark_all_read(&mut self, cx: &mut Context) { + for item in &mut self.items { + item.read = true; + } + cx.notify(); + } + + /// 删除一条通知;删的是展开项则收起详情。 + pub fn remove(&mut self, id: u64, cx: &mut Context) { + self.items.retain(|item| item.id != id); + if self.selected == Some(id) { + self.selected = None; + } + cx.notify(); + } + + /// 清空全部通知。 + pub fn clear_all(&mut self, cx: &mut Context) { + self.items.clear(); + self.selected = None; + cx.notify(); + } + + /// 点击行:标已读 + 展开详情(对齐 Tauri 打开详情;诊断类额外跳文件)。 + /// 行内删除按钮冒泡上来时条目已不存在,此时不留过期选中。 + fn open_details(&mut self, id: u64, cx: &mut Context) { + if let Some(item) = self.items.iter_mut().find(|item| item.id == id) { + item.read = true; + self.selected = Some(id); + } + cx.notify(); + } + + /// 懒创建搜索输入框(首次渲染创建,后续渲染复用用户编辑态)。 + fn ensure_search(&mut self, window: &mut Window, cx: &mut Context) -> Entity { + if let Some(entity) = self.search.clone() { + return entity; + } + let entity = cx.new(|cx| InputState::new(window, cx)); + self.search = Some(entity.clone()); + entity + } + + fn query(&self, cx: &Context) -> String { + self.search + .as_ref() + .map(|entity| entity.read(cx).value().to_string()) + .unwrap_or_default() + .trim() + .to_lowercase() + } +} + +fn kind_icon(kind: NotificationKind) -> IconName { + match kind { + NotificationKind::Info => IconName::Info, + NotificationKind::Success => IconName::CircleCheck, + NotificationKind::Warning | NotificationKind::Error | NotificationKind::Diagnostic => { + IconName::TriangleAlert + } + } +} + +impl Render for NotificationsView { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + // 快照后构建行,避免在 `.children` 闭包里同时借用 self 与 cx。 + let search_entity = self.ensure_search(window, cx); + let query = self.query(cx); + let unread = self.unread_count(); + let filter = self.filter; + let view = cx.entity(); + let filter_options: Vec<(&'static str, String)> = NotificationFilter::ALL + .iter() + .map(|kind| { + ( + kind.as_str(), + crate::i18n::menu_text(cx, kind.label_key()).to_string(), + ) + }) + .collect(); + let selected = self.selected; + let items: Vec = self + .items + .iter() + .filter(|item| filter.matches(item.kind)) + .filter(|item| { + query.is_empty() + || item.title.to_lowercase().contains(&query) + || item.body.to_lowercase().contains(&query) + }) + .cloned() + .collect(); + let is_empty = self.items.is_empty(); + + let mut rows = Vec::new(); + for item in &items { + let id = item.id; + let path = item.path.clone(); + let line = item.line; + let is_diagnostic = item.kind == NotificationKind::Diagnostic; + let expanded = selected == Some(id); + rows.push( + v_flex() + .id(format!("notification-{id}")) + .w_full() + .cursor_pointer() + .rounded_sm() + .px_2() + .py_1() + .gap_1() + .hover(|row| row.bg(ThemeColors::bg_tab_hover())) + .when(!item.read, |row| row.bg(ThemeColors::subtle_selection())) + .when(expanded, |row| row.bg(ThemeColors::subtle_selection())) + .child( + h_flex() + .w_full() + .items_center() + .gap_1p5() + .child(Icon::new(kind_icon(item.kind)).size(px(14.0)).text_color( + if item.kind == NotificationKind::Error || is_diagnostic { + ThemeColors::destructive() + } else { + ThemeColors::text_muted() + }, + )) + .child( + div() + .flex_1() + .truncate() + .text_xs() + .text_color(ThemeColors::text_primary()) + .when(!item.read, |title| title.font_weight(FontWeight::BOLD)) + .child(item.title.clone()), + ) + .when(!item.read, |row| { + row.child( + div() + .w(px(6.0)) + .h(px(6.0)) + .rounded_full() + .bg(ThemeColors::accent_blue()) + .flex_shrink_0(), + ) + }), + ) + .child( + div() + .w_full() + .pl(px(20.0)) + .truncate() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(item.body.clone()), + ) + .when(expanded, |row| { + row.child( + v_flex() + .w_full() + .pl(px(20.0)) + .gap_1p5() + .py_1() + .child( + div() + .text_xs() + .font_weight(FontWeight::BOLD) + .text_color(ThemeColors::text_muted()) + .child(crate::i18n::menu_text(cx, "notifications.details")), + ) + .child( + div() + .w_full() + .text_xs() + .text_color(ThemeColors::text_primary()) + .child(item.body.clone()), + ) + .child( + Button::new(format!("notification-delete-{id}")) + .small() + .ghost() + .icon(IconName::Trash) + .label(crate::i18n::menu_text(cx, "notifications.delete")) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.remove(id, cx); + })), + ), + ) + }) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.open_details(id, cx); + if is_diagnostic { + if let Some(path) = path.clone() { + cx.emit(NotificationsEvent::OpenFile(path, line)); + } + } + })) + .into_any_element(), + ); + } + + v_flex() + .size_full() + .bg(ThemeColors::bg_sidebar()) + .border_l_1() + .border_color(ThemeColors::border()) + .child( + h_flex() + .h(px(32.0)) + .w_full() + .bg(ThemeColors::bg_sidebar()) + .border_b_1() + .border_color(ThemeColors::border()) + .items_center() + .justify_between() + .px_3() + .child( + h_flex() + .items_center() + .gap_1p5() + .child( + div() + .text_xs() + .font_weight(FontWeight::BOLD) + .text_color(ThemeColors::text_muted()) + .child(crate::i18n::menu_text(cx, "notifications.title")), + ) + .when(unread > 0, |title| { + title.child( + div() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(format!("({unread})")), + ) + }), + ) + .child( + h_flex() + .items_center() + .gap_1() + .when(unread > 0, |actions| { + actions.child( + Button::new("notifications-mark-read") + .small() + .ghost() + .label(crate::i18n::menu_text( + cx, + "notifications.markAllRead", + )) + .on_click(cx.listener(|this, _event, _window, cx| { + this.mark_all_read(cx); + })), + ) + }) + .when(!is_empty, |actions| { + actions.child( + Button::new("notifications-clear-all") + .small() + .ghost() + .icon(IconName::Trash) + .tooltip(crate::i18n::menu_text( + cx, + "notifications.clearAll", + )) + .on_click(cx.listener(|this, _event, _window, cx| { + this.clear_all(cx); + })), + ) + }) + .child( + Button::new("notifications-close") + .small() + .ghost() + .icon(IconName::Close) + .tooltip(crate::i18n::menu_text(cx, "ui.close")) + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(NotificationsEvent::Close); + })), + ), + ), + ) + .child( + h_flex() + .w_full() + .items_center() + .gap_1p5() + .px_2() + .py_2() + .child( + Icon::new(IconName::Search) + .size(px(13.0)) + .text_color(ThemeColors::text_muted()), + ) + .child( + div() + .flex_1() + .child(Input::new(&search_entity).cleanable(true)), + ) + .child( + Button::new("notifications-filter") + .small() + .ghost() + .icon(IconName::Funnel) + .tooltip(crate::i18n::menu_text(cx, "notifications.filter")) + .dropdown_menu(move |menu, _window, _cx| { + let mut menu = menu; + for (value, label) in &filter_options { + let v = view.clone(); + let value = *value; + let label = label.clone(); + let selected = filter.as_str() == value; + let item = gpui_kit::component::menu::PopupMenuItem::new(label); + let item = if selected { + item.icon(IconName::Check) + } else { + item + }; + menu = menu.item(item.on_click(move |_, _, cx| { + v.update(cx, |this, cx| { + if let Some(kind) = NotificationFilter::from_str(value) + { + this.filter = kind; + cx.notify(); + } + }); + })); + } + menu + }), + ), + ) + .child(if is_empty { + div() + .flex_1() + .w_full() + .flex() + .items_center() + .justify_center() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(crate::i18n::menu_text(cx, "notifications.empty")) + .into_any_element() + } else if items.is_empty() { + div() + .flex_1() + .w_full() + .flex() + .items_center() + .justify_center() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(crate::i18n::menu_text(cx, "notifications.noMatch")) + .into_any_element() + } else { + div() + .flex_1() + .w_full() + .overflow_y_scrollbar() + .px_2() + .py_1() + .children(rows) + .into_any_element() + }) + } +} diff --git a/linux/src/workbench/panes.rs b/linux/src/workbench/panes.rs new file mode 100644 index 000000000..81c401004 --- /dev/null +++ b/linux/src/workbench/panes.rs @@ -0,0 +1,357 @@ +//! 窗格树纯逻辑:对齐 Tauri `features/panes` 的拆分与路由语义。 +//! +//! 窗格树可任意嵌套左右/上下拆分([`SplitDir::Horizontal`] 为左右并排, +//! [`SplitDir::Vertical`] 为上下堆叠)。锁定窗格不接收新文件: +//! [`PaneTree::route_target`] 在 active 未锁定时返回 active,否则返回 +//! 第一个未锁定叶,全锁定时返回 `None`(对齐 `pane-routing.ts` +//! `resolveWritablePaneForBuffer` 的回退语义)。 +//! +//! 本模块不依赖 gpui,只表达树形结构与路由规则,方便单测。 + +use std::collections::HashSet; + +/// 窗格标识:[`PaneTree::new`] 从 0 起按拆分顺序递增分配。 +pub type PaneId = u64; + +/// 拆分方向:`Horizontal` 为左右并排,`Vertical` 为上下堆叠。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SplitDir { + /// 左右并排。 + Horizontal, + /// 上下堆叠。 + Vertical, +} + +/// 窗格树节点:叶为单个窗格,`Split` 为一次拆分(`first` 为原叶所在侧, +/// `second` 为新叶所在侧,`ratio` 为 `first` 所占比例)。 +#[derive(Debug, Clone, PartialEq)] +pub enum PaneNode { + /// 单个窗格。 + Leaf(PaneId), + /// 一次拆分。 + Split { + /// 拆分方向。 + dir: SplitDir, + /// 原叶所在侧。 + first: Box, + /// 新叶所在侧。 + second: Box, + /// `first` 所占比例,写入时钳制在 `0.1..=0.9`。 + ratio: f32, + }, +} + +/// 比例下界:所有写入 [`PaneNode::Split::ratio`] 的值都钳制到 +/// `MIN_RATIO..=MAX_RATIO`。 +const MIN_RATIO: f32 = 0.1; +/// 比例上界,见 [`MIN_RATIO`]。 +const MAX_RATIO: f32 = 0.9; + +/// 窗格树:持有根节点、下个待分配 id、活动窗格与锁定集合。 +#[derive(Debug, Clone)] +pub struct PaneTree { + root: Option, + next_id: u64, + active: Option, + locked: HashSet, +} + +impl PaneTree { + /// 新建单叶树:叶 id 为 0,active 为 `Some(0)`。 + pub fn new() -> Self { + Self { + root: Some(PaneNode::Leaf(0)), + next_id: 1, + active: Some(0), + locked: HashSet::new(), + } + } + + /// 当前活动窗格(关闭后会自动跟随到存活叶)。 + pub fn active(&self) -> Option { + self.active + } + + /// 根节点只读视图(渲染层递归用)。 + pub fn root(&self) -> Option<&PaneNode> { + self.root.as_ref() + } + + /// 先序收集所有叶 id(`Split` 先 `first` 后 `second`)。 + pub fn leaves(&self) -> Vec { + let mut out = Vec::new(); + if let Some(root) = &self.root { + collect_leaves(root, &mut out); + } + out + } + + /// 拆分叶 `id`:原叶变为 `Split` 的 `first`,新叶为 `second`, + /// 比例为 0.5;返回新叶 id,叶不存在时返回 `None`。 + pub fn split_leaf(&mut self, id: PaneId, dir: SplitDir) -> Option { + let slot = self + .root + .as_mut() + .and_then(|root| find_leaf_mut(root, id))?; + let new_id = self.next_id; + self.next_id += 1; + *slot = PaneNode::Split { + dir, + first: Box::new(PaneNode::Leaf(id)), + second: Box::new(PaneNode::Leaf(new_id)), + ratio: 0.5, + }; + Some(new_id) + } + + /// 关闭叶 `id`:兄弟子树顶替父 `Split`;只剩根叶时保留不删返回 + /// `false`,叶不存在时同样返回 `false`。关闭的叶若为 active, + /// active 指向顶替兄弟的首叶(就近存活叶)。 + pub fn close_leaf(&mut self, id: PaneId) -> bool { + if matches!(self.root, Some(PaneNode::Leaf(root_id)) if root_id == id) { + return false; + } + let fallback = match self.root.as_mut() { + Some(root) => delete_leaf(root, id), + None => None, + }; + let Some(fallback) = fallback else { + return false; + }; + self.locked.remove(&id); + if self.active == Some(id) { + self.active = Some(fallback); + } + true + } + + /// 按叶子归属定位其父 `Split` 并更新比例(钳制在 `0.1..=0.9`); + /// 根叶或叶不存在时返回 `false`。 + pub fn set_ratio(&mut self, leaf: PaneId, ratio: f32) -> bool { + let parent = self + .root + .as_mut() + .and_then(|root| find_parent_split_mut(root, leaf)); + match parent { + Some(PaneNode::Split { ratio: slot, .. }) => { + *slot = clamp_ratio(ratio); + true + } + _ => false, + } + } + + /// 叶存在时设为 active 并返回 `true`,否则保持原值返回 `false`。 + pub fn set_active(&mut self, id: PaneId) -> bool { + if self.leaves().contains(&id) { + self.active = Some(id); + true + } else { + false + } + } + + /// 翻转叶 `id` 的锁定态并返回翻转后状态;叶不存在时不记录返回 `false`。 + pub fn toggle_lock(&mut self, id: PaneId) -> bool { + if !self.leaves().contains(&id) { + return false; + } + if self.locked.contains(&id) { + self.locked.remove(&id); + false + } else { + self.locked.insert(id); + true + } + } + + /// 叶 `id` 是否被锁定(不存在的叶返回 `false`)。 + pub fn is_locked(&self, id: PaneId) -> bool { + self.locked.contains(&id) + } + + /// 新文件路由目标:active 存在且未锁定时返回 active,否则返回 + /// 第一个未锁定叶;全部锁定时返回 `None`。 + pub fn route_target(&self) -> Option { + let leaves = self.leaves(); + if let Some(active) = self.active { + if leaves.contains(&active) && !self.locked.contains(&active) { + return Some(active); + } + } + leaves.into_iter().find(|id| !self.locked.contains(id)) + } +} + +/// 空树默认值(`root` 为空,`active` 为空);常规入口仍用 [`PaneTree::new`]。 +impl Default for PaneTree { + fn default() -> Self { + Self { + root: None, + next_id: 0, + active: None, + locked: HashSet::new(), + } + } +} + +/// 比例钳制到 `MIN_RATIO..=MAX_RATIO`。 +fn clamp_ratio(ratio: f32) -> f32 { + ratio.clamp(MIN_RATIO, MAX_RATIO) +} + +/// 先序收集叶 id。 +fn collect_leaves(node: &PaneNode, out: &mut Vec) { + match node { + PaneNode::Leaf(id) => out.push(*id), + PaneNode::Split { first, second, .. } => { + collect_leaves(first, out); + collect_leaves(second, out); + } + } +} + +/// 子树首叶(先序第一个叶)。 +fn first_leaf(node: &PaneNode) -> Option { + match node { + PaneNode::Leaf(id) => Some(*id), + PaneNode::Split { first, .. } => first_leaf(first), + } +} + +/// 在子树中定位叶 `id` 的可写槽。 +fn find_leaf_mut(node: &mut PaneNode, id: PaneId) -> Option<&mut PaneNode> { + match node { + PaneNode::Leaf(leaf_id) if *leaf_id == id => Some(node), + PaneNode::Leaf(_) => None, + PaneNode::Split { first, second, .. } => { + find_leaf_mut(first, id).or_else(|| find_leaf_mut(second, id)) + } + } +} + +/// 叶 `id` 的父 `Split`(根叶无父,返回 `None`)。 +/// +/// 先用不可变借用判直属(借用即时结束),再决定返回 `node` 或向下递归, +/// 避免可变借用与返回值的生命周期冲突。 +fn find_parent_split_mut(node: &mut PaneNode, leaf: PaneId) -> Option<&mut PaneNode> { + fn is_leaf_id(node: &PaneNode, leaf: PaneId) -> bool { + matches!(node, PaneNode::Leaf(id) if *id == leaf) + } + let is_direct_child = match node { + PaneNode::Leaf(_) => return None, + PaneNode::Split { first, second, .. } => { + is_leaf_id(first, leaf) || is_leaf_id(second, leaf) + } + }; + if is_direct_child { + return Some(node); + } + match node { + PaneNode::Leaf(_) => None, + PaneNode::Split { first, second, .. } => { + find_parent_split_mut(first, leaf).or_else(|| find_parent_split_mut(second, leaf)) + } + } +} + +/// 删除子树中的叶 `id`,兄弟子树顶替父 `Split`;成功返回顶替兄弟的首叶。 +fn delete_leaf(node: &mut PaneNode, id: PaneId) -> Option { + let PaneNode::Split { first, second, .. } = node else { + return None; + }; + if matches!(**first, PaneNode::Leaf(leaf_id) if leaf_id == id) { + let fallback = first_leaf(second); + let mut empty = PaneNode::Leaf(0); + std::mem::swap(&mut empty, second); + *node = empty; + return fallback; + } + if matches!(**second, PaneNode::Leaf(leaf_id) if leaf_id == id) { + let fallback = first_leaf(first); + let mut empty = PaneNode::Leaf(0); + std::mem::swap(&mut empty, first); + *node = empty; + return fallback; + } + delete_leaf(first, id).or_else(|| delete_leaf(second, id)) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// 拆分:原叶变 `first`,新叶为 `second`,比例 0.5。 + #[test] + fn split_leaf_appends_second_leaf() { + let mut tree = PaneTree::new(); + assert_eq!(tree.split_leaf(0, SplitDir::Horizontal), Some(1)); + assert_eq!(tree.leaves(), vec![0, 1]); + assert!(matches!( + tree.root, + Some(PaneNode::Split { + dir: SplitDir::Horizontal, + ratio, + .. + }) if ratio == 0.5 + )); + assert_eq!(tree.split_leaf(99, SplitDir::Vertical), None); + } + + /// 关闭:兄弟顶替父节点;只剩根叶时保留不删返回 `false`。 + #[test] + fn close_leaf_collapses_sibling_and_keeps_last_root() { + let mut tree = PaneTree::new(); + let second = tree.split_leaf(0, SplitDir::Vertical).unwrap(); + assert!(!tree.close_leaf(99)); + assert!(tree.close_leaf(second)); + assert_eq!(tree.leaves(), vec![0]); + assert!(!tree.close_leaf(0)); + assert_eq!(tree.leaves(), vec![0]); + } + + /// 关闭 active 叶时 active 跟随到就近存活叶。 + #[test] + fn close_active_leaf_moves_active_to_survivor() { + let mut tree = PaneTree::new(); + let second = tree.split_leaf(0, SplitDir::Horizontal).unwrap(); + assert!(tree.set_active(second)); + assert!(tree.close_leaf(second)); + assert_eq!(tree.active(), Some(0)); + assert_eq!(tree.route_target(), Some(0)); + } + + /// 路由:active 未锁定返回 active;锁定后回退首个未锁叶;全锁返回 `None`。 + #[test] + fn route_target_prefers_unlocked_active_then_falls_back() { + let mut tree = PaneTree::new(); + let second = tree.split_leaf(0, SplitDir::Horizontal).unwrap(); + assert_eq!(tree.route_target(), Some(0)); + assert!(tree.toggle_lock(0)); + assert!(tree.is_locked(0)); + assert_eq!(tree.route_target(), Some(second)); + tree.toggle_lock(second); + assert_eq!(tree.route_target(), None); + } + + /// 比例钳制在 `0.1..=0.9`。 + #[test] + fn set_ratio_clamps_to_bounds() { + let mut tree = PaneTree::new(); + tree.split_leaf(0, SplitDir::Horizontal).unwrap(); + assert!(tree.set_ratio(0, 10.0)); + assert_eq!(root_ratio(&tree), Some(0.9)); + assert!(tree.set_ratio(1, -2.0)); + assert_eq!(root_ratio(&tree), Some(0.1)); + assert!(tree.set_ratio(0, 0.3)); + assert_eq!(root_ratio(&tree), Some(0.3)); + } + + /// 测试辅助:读取根 `Split` 的比例(只读私有字段,单测内可访问)。 + fn root_ratio(tree: &PaneTree) -> Option { + match &tree.root { + Some(PaneNode::Split { ratio, .. }) => Some(*ratio), + _ => None, + } + } +} diff --git a/linux/src/workbench/project_dialog.rs b/linux/src/workbench/project_dialog.rs new file mode 100644 index 000000000..4512fc8b9 --- /dev/null +++ b/linux/src/workbench/project_dialog.rs @@ -0,0 +1,1344 @@ +//! 新建/克隆项目对话框:复刻 Tauri 端 `new-project-content.tsx` 的两步流程。 +//! +//! - New 模式:source 列表(搜索 + 3 个来源行)→ details 表单 → 创建。 +//! - Clone 模式:直接进 details(仓库 URL + 自动推导名称 + 位置)。 +//! - 名称校验、URL 推导名、starter 命令逐条移植自 +//! `windows/tauri/src/features/window/lib/new-project-model.ts`。 +//! - 文案键与 `windows/tauri/src/i18n/locale.ts` 的 `newProject.*` / +//! `welcome.*` 对齐,`{path}` / `{query}` 占位在渲染时替换。 +//! - 遮罩 + 卡片布局仿 `settings_dialog.rs`,行样式与键盘输入仿 +//! `sidebar.rs` / `quick_open.rs`(只读展示行 + `on_key_down` 追加)。 +//! - 执行:空项目同步 `create_dir_all`;模板项目同步建目录并把 starter +//! 命令随事件交给父级发往新终端;clone 在 +//! `cx.background_executor()` 后台跑 `git clone`,避免阻塞 UI。 + +use gpui_kit::assets::IconName; +use gpui_kit::component::button::{Button, ButtonVariants as _}; +use gpui_kit::component::scroll::ScrollableElement as _; +use gpui_kit::component::{h_flex, v_flex, Disableable as _, Icon, Sizable as _}; +use gpui_kit::prelude::FluentBuilder as _; +use gpui_kit::{ + div, px, rgba, AnyElement, Context, EventEmitter, FocusHandle, FontWeight, + InteractiveElement as _, IntoElement, KeyDownEvent, ParentElement as _, Render, + StatefulInteractiveElement as _, Styled as _, Window, +}; + +/// 对话框模式:新建项目向导,或直接克隆仓库。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProjectDialogMode { + New, + Clone, +} + +/// 向导步骤:来源选择 → 详情表单 → 创建中(Clone 模式跳过来源选择)。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ProjectDialogStep { + Source, + Details, + Creating, +} + +/// 新建来源(clone 走 [`ProjectDialogMode::Clone`],不在 New 列表中)。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NewProjectSource { + Empty, + Nextjs, + ViteReact, +} + +/// 包管理器:详情页循环切换按钮在三者之间轮换。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PackageManager { + Npm, + Pnpm, + Bun, +} + +impl PackageManager { + /// 按钮展示文本,与 Tauri `packageManagerOptions` 的 label 一致。 + fn label(self) -> &'static str { + match self { + PackageManager::Npm => "npm", + PackageManager::Pnpm => "pnpm", + PackageManager::Bun => "Bun", + } + } +} + +/// 当前接收键盘输入的字段(对话框内只有一个焦点句柄,按点击行切换)。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ActiveField { + Search, + ProjectName, + RepositoryUrl, + Location, +} + +/// 对话框对外事件。 +#[derive(Debug, Clone)] +pub enum ProjectDialogEvent { + /// 项目已就绪:`path` 为新项目目录;模板项目附带 starter 命令, + /// 父级打开目录后应把该命令原样发往新终端。 + OpenedProject { + path: String, + starter: Option, + }, + /// 请求关闭对话框。 + Close, +} + +/// Windows 保留名(不含扩展名的首段命中即跨平台不合法)。 +const WINDOWS_RESERVED_NAMES: &[&str] = &[ + "con", "prn", "aux", "nul", "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", + "com9", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", +]; + +/// 文件名保留字符(含路径分隔符)。 +const RESERVED_CHARS: [char; 9] = ['<', '>', ':', '"', '/', '\\', '|', '?', '*']; + +/// 居中新建项目面板(宽 560px,高 480px)。 +pub struct ProjectDialog { + mode: ProjectDialogMode, + step: ProjectDialogStep, + source: NewProjectSource, + /// 来源搜索串。 + query: String, + selected_index: usize, + project_name: String, + repository_url: String, + location_path: String, + package_manager: PackageManager, + /// 名称是否被用户亲手改过;未改过时 URL 变化会自动推导名称。 + name_was_edited: bool, + error_message: Option, + active_field: ActiveField, + focus_handle: FocusHandle, +} + +impl EventEmitter for ProjectDialog {} + +impl ProjectDialog { + /// 新建对话框:位置默认取 `$HOME`(不引入新依赖,不读真实 home 库)。 + pub fn new(cx: &mut Context) -> Self { + Self { + mode: ProjectDialogMode::New, + step: ProjectDialogStep::Source, + source: NewProjectSource::Empty, + query: String::new(), + selected_index: 0, + project_name: String::new(), + repository_url: String::new(), + location_path: std::env::var("HOME").unwrap_or_default(), + package_manager: PackageManager::Npm, + name_was_edited: false, + error_message: None, + active_field: ActiveField::Search, + focus_handle: cx.focus_handle(), + } + } + + /// 切换模式并重置整个表单(New 进来源选择,Clone 直接进详情)。 + pub fn set_mode(&mut self, mode: ProjectDialogMode, cx: &mut Context) { + self.mode = mode; + self.step = match mode { + ProjectDialogMode::New => ProjectDialogStep::Source, + ProjectDialogMode::Clone => ProjectDialogStep::Details, + }; + self.source = NewProjectSource::Empty; + self.query.clear(); + self.selected_index = 0; + self.project_name.clear(); + self.repository_url.clear(); + self.location_path = std::env::var("HOME").unwrap_or_default(); + self.package_manager = PackageManager::Npm; + self.name_was_edited = false; + self.error_message = None; + self.active_field = match mode { + ProjectDialogMode::New => ActiveField::Search, + ProjectDialogMode::Clone => ActiveField::RepositoryUrl, + }; + cx.notify(); + } + + /// 系统目录选择框;取消/失败(含无 zenity 等后端缺失)返回 `None`, + /// 调用方一律视为取消。 + pub fn pick_folder(initial: Option<&str>) -> Option { + let mut dialog = rfd::FileDialog::new(); + if let Some(dir) = initial.map(str::trim).filter(|s| !s.is_empty()) { + dialog = dialog.set_directory(dir); + } + dialog + .pick_folder() + .map(|path| path.to_string_lossy().to_string()) + } + + /// 移植 `getProjectNameError`:返回 i18n 键,渲染时再解析为文案。 + fn name_error_key(&self) -> Option<&'static str> { + let name = self.project_name.trim(); + if name.is_empty() { + return Some("newProject.errorEnterName"); + } + if name == "." || name == ".." { + return Some("newProject.errorPathTraversalName"); + } + if name + .chars() + .any(|c| (c as u32) < 32 || RESERVED_CHARS.contains(&c)) + { + return Some("newProject.errorReservedCharacters"); + } + if name.ends_with('.') || name.ends_with(' ') { + return Some("newProject.errorTrailingPeriodOrSpace"); + } + let base = name.split('.').next().unwrap_or("").to_lowercase(); + if WINDOWS_RESERVED_NAMES.contains(&base.as_str()) { + return Some("newProject.errorCrossPlatformName"); + } + None + } + + /// 移植 `inferProjectNameFromRepositoryUrl`:去尾斜杠、取末段、去 `.git` + /// (大小写不敏感)。无 URL 解码依赖,跳过 `decodeURIComponent`。 + fn infer_name_from_url(url: &str) -> String { + let trimmed = url.trim().trim_end_matches(['/', '\\']); + if trimmed.is_empty() { + return String::new(); + } + let last = trimmed + .rsplit(|c| c == '/' || c == ':' || c == '\\') + .find(|segment| !segment.is_empty()) + .unwrap_or(""); + if last.len() >= 4 && last[last.len() - 4..].eq_ignore_ascii_case(".git") { + last[..last.len() - 4].to_string() + } else { + last.to_string() + } + } + + /// 移植 `getStarterCommand`:命令字符串与 `new-project-model.ts` 逐字一致。 + fn starter_command(source: NewProjectSource, pm: PackageManager) -> Option { + match source { + NewProjectSource::Empty => None, + NewProjectSource::Nextjs => { + let runner = match pm { + PackageManager::Npm => "npx --yes", + PackageManager::Pnpm => "pnpm dlx", + PackageManager::Bun => "bunx", + }; + let flag = match pm { + PackageManager::Npm => "--use-npm", + PackageManager::Pnpm => "--use-pnpm", + PackageManager::Bun => "--use-bun", + }; + Some(format!( + "{runner} create-next-app@latest . --typescript --tailwind --eslint --app --src-dir --import-alias \"@/*\" {flag}" + )) + } + NewProjectSource::ViteReact => { + let create = match pm { + PackageManager::Npm => "npm create vite@latest . -- --template react-ts", + PackageManager::Pnpm => "pnpm create vite@latest . --template react-ts", + PackageManager::Bun => "bun create vite@latest . --template react-ts", + }; + let install = match pm { + PackageManager::Npm => "npm install", + PackageManager::Pnpm => "pnpm install", + PackageManager::Bun => "bun install", + }; + Some(format!("{create} && {install}")) + } + } + } + + /// 目标目录 = 位置 + 名称(对齐 `getNewProjectPath` 的拼接语义)。 + fn destination_path(&self) -> String { + let base = self.location_path.trim().trim_end_matches('/'); + let name = self.project_name.trim(); + if base.is_empty() { + return name.to_string(); + } + format!("{base}/{name}") + } + + /// 是否允许创建(对齐 Tauri `canCreate`)。 + fn can_create(&self) -> bool { + if self.step != ProjectDialogStep::Details { + return false; + } + if self.name_error_key().is_some() { + return false; + } + if self.location_path.trim().is_empty() { + return false; + } + if self.mode == ProjectDialogMode::Clone && self.repository_url.trim().is_empty() { + return false; + } + true + } + + /// 选中来源并进入详情表单。 + fn choose_source(&mut self, source: NewProjectSource, cx: &mut Context) { + self.source = source; + self.step = ProjectDialogStep::Details; + self.project_name.clear(); + self.repository_url.clear(); + self.name_was_edited = false; + self.error_message = None; + self.active_field = ActiveField::ProjectName; + cx.notify(); + } + + /// 从详情返回来源选择。 + fn back_to_source(&mut self, cx: &mut Context) { + self.step = ProjectDialogStep::Source; + self.query.clear(); + self.selected_index = 0; + self.error_message = None; + self.active_field = ActiveField::Search; + cx.notify(); + } + + /// 包管理器循环切换:npm → pnpm → Bun → npm。 + fn cycle_package_manager(&mut self, cx: &mut Context) { + self.package_manager = match self.package_manager { + PackageManager::Npm => PackageManager::Pnpm, + PackageManager::Pnpm => PackageManager::Bun, + PackageManager::Bun => PackageManager::Npm, + }; + cx.notify(); + } + + /// 用系统对话框重选父目录;取消则保持原值。 + fn choose_location(&mut self, cx: &mut Context) { + if let Some(dir) = Self::pick_folder(Some(self.location_path.trim())) { + self.location_path = dir; + self.error_message = None; + cx.notify(); + } + } + + /// 往当前输入字段追加文本(URL 变化时自动推导名称)。 + fn push_text(&mut self, text: &str) { + match self.active_field { + ActiveField::Search => { + self.query.push_str(text); + self.selected_index = 0; + } + ActiveField::ProjectName => { + self.project_name.push_str(text); + self.name_was_edited = true; + self.error_message = None; + } + ActiveField::RepositoryUrl => { + self.repository_url.push_str(text); + self.error_message = None; + if !self.name_was_edited { + self.project_name = Self::infer_name_from_url(&self.repository_url); + } + } + ActiveField::Location => { + self.location_path.push_str(text); + self.error_message = None; + } + } + } + + /// 当前输入字段退格(URL 退格同样触发名称重推导)。 + fn pop_text(&mut self) { + match self.active_field { + ActiveField::Search => { + self.query.pop(); + self.selected_index = 0; + } + ActiveField::ProjectName => { + self.project_name.pop(); + self.error_message = None; + } + ActiveField::RepositoryUrl => { + self.repository_url.pop(); + self.error_message = None; + if !self.name_was_edited { + self.project_name = Self::infer_name_from_url(&self.repository_url); + } + } + ActiveField::Location => { + self.location_path.pop(); + self.error_message = None; + } + } + } + + /// 执行创建: + /// - Empty / 模板:目标存在即报错,否则同步建目录后发射事件 + /// (模板附带 starter 命令,父级负责发往新终端); + /// - Clone:先进 Creating,再后台跑 `git clone`,成功发射事件, + /// 失败回 Details 并显示 stderr。 + fn begin_create(&mut self, cx: &mut Context) { + if !self.can_create() { + return; + } + let dest = self.destination_path(); + if std::path::Path::new(&dest).exists() { + let template = crate::i18n::menu_text(cx, "newProject.errorDestinationExists"); + self.error_message = Some(template.replace("{path}", &dest)); + cx.notify(); + return; + } + + if self.mode == ProjectDialogMode::Clone { + self.step = ProjectDialogStep::Creating; + self.error_message = None; + cx.notify(); + let url = self.repository_url.trim().to_string(); + let dest_for_clone = dest.clone(); + cx.spawn(async move |this, cx| { + let output = cx + .background_executor() + .spawn(async move { + std::process::Command::new("git") + .args(["clone", url.as_str(), dest_for_clone.as_str()]) + .output() + }) + .await; + match output { + Ok(result) if result.status.success() => { + let _ = this.update(cx, |_this, cx| { + cx.emit(ProjectDialogEvent::OpenedProject { + path: dest, + starter: None, + }); + }); + } + Ok(result) => { + let stderr = String::from_utf8_lossy(&result.stderr).trim().to_string(); + let message = if stderr.is_empty() { + format!("git clone failed: {}", result.status) + } else { + stderr + }; + let _ = this.update(cx, |this, cx| { + this.step = ProjectDialogStep::Details; + this.error_message = Some(message); + cx.notify(); + }); + } + Err(err) => { + let message = format!("git clone failed: {err}"); + let _ = this.update(cx, |this, cx| { + this.step = ProjectDialogStep::Details; + this.error_message = Some(message); + cx.notify(); + }); + } + } + }) + .detach(); + return; + } + + match std::fs::create_dir_all(&dest) { + Ok(()) => { + let starter = Self::starter_command(self.source, self.package_manager); + cx.emit(ProjectDialogEvent::OpenedProject { + path: dest, + starter, + }); + } + Err(err) => { + self.error_message = Some(err.to_string()); + cx.notify(); + } + } + } + + /// New 模式来源选择项(Clone 不在列表中)。 + fn filtered_ids(&self, cx: &gpui_kit::App) -> Vec { + let query = self.query.trim().to_lowercase(); + const ALL: [NewProjectSource; 3] = [ + NewProjectSource::Empty, + NewProjectSource::Nextjs, + NewProjectSource::ViteReact, + ]; + if query.is_empty() { + return ALL.to_vec(); + } + ALL.into_iter() + .filter(|source| { + let label = source.label(cx).to_lowercase(); + let desc = crate::i18n::menu_text(cx, source.description_key()).to_lowercase(); + label.contains(query.as_str()) + || desc.contains(query.as_str()) + || source + .keywords() + .iter() + .any(|kw| kw.contains(query.as_str())) + }) + .collect() + } +} + +impl NewProjectSource { + /// 来源图标。 + fn icon(self) -> IconName { + match self { + NewProjectSource::Empty => IconName::Folder, + NewProjectSource::Nextjs => IconName::Package, + NewProjectSource::ViteReact => IconName::Code, + } + } + + /// 来源标题:Empty 走 i18n,模板用固定英文产品名。 + fn label(self, cx: &gpui_kit::App) -> String { + match self { + NewProjectSource::Empty => { + crate::i18n::menu_text(cx, "welcome.emptyProject").to_string() + } + NewProjectSource::Nextjs => "Next.js".to_string(), + NewProjectSource::ViteReact => "Vite + React".to_string(), + } + } + + /// 来源描述 i18n 键。 + fn description_key(self) -> &'static str { + match self { + NewProjectSource::Empty => "welcome.emptyProjectDescription", + NewProjectSource::Nextjs => "welcome.nextjsDescription", + NewProjectSource::ViteReact => "welcome.viteReactDescription", + } + } + + /// 来源徽标 i18n 键。 + fn badge_key(self) -> &'static str { + match self { + NewProjectSource::Empty => "welcome.builtIn", + NewProjectSource::Nextjs | NewProjectSource::ViteReact => "welcome.webApp", + } + } + + /// 搜索关键词(与 Tauri `keywords` 一致)。 + fn keywords(self) -> &'static [&'static str] { + match self { + NewProjectSource::Empty => &["blank", "folder", "local", "empty"], + NewProjectSource::Nextjs => &["react", "typescript", "tailwind", "frontend"], + NewProjectSource::ViteReact => &["react", "typescript", "frontend"], + } + } +} + +impl Render for ProjectDialog { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + // 请求聚焦以接收按键输入(与 quick_open 相同)。 + window.focus(&self.focus_handle, cx); + + let is_creating = self.step == ProjectDialogStep::Creating; + let is_clone = self.mode == ProjectDialogMode::Clone; + + // 全屏半透明遮罩:创建中禁用关闭,点击空白处关闭。 + div() + .id("project-dialog-backdrop") + .track_focus(&self.focus_handle) + .absolute() + .inset_0() + .bg(rgba(0x00000088)) + .flex() + .items_center() + .justify_center() + .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { + if this.step == ProjectDialogStep::Creating { + return; + } + let key = event.keystroke.key.as_str(); + match key { + "escape" => match (this.mode, this.step) { + (ProjectDialogMode::New, ProjectDialogStep::Details) => { + this.back_to_source(cx); + } + _ => cx.emit(ProjectDialogEvent::Close), + }, + "enter" => match this.step { + ProjectDialogStep::Source => { + let ids = this.filtered_ids(cx); + let index = this.selected_index.min(ids.len().saturating_sub(1)); + if let Some(id) = ids.get(index).copied() { + this.choose_source(id, cx); + } + } + ProjectDialogStep::Details => this.begin_create(cx), + ProjectDialogStep::Creating => {} + }, + "up" | "arrowup" => { + if this.step == ProjectDialogStep::Source { + let total = this.filtered_ids(cx).len(); + if total > 0 { + this.selected_index = (this.selected_index + total - 1) % total; + cx.notify(); + } + } + } + "down" | "arrowdown" => { + if this.step == ProjectDialogStep::Source { + let total = this.filtered_ids(cx).len(); + if total > 0 { + this.selected_index = (this.selected_index + 1) % total; + cx.notify(); + } + } + } + "backspace" => { + // 来源选择页的退格改搜索串,其余页改当前字段。 + if this.step == ProjectDialogStep::Source { + this.active_field = ActiveField::Search; + } + this.pop_text(); + cx.notify(); + } + "space" => { + if this.step == ProjectDialogStep::Source { + this.active_field = ActiveField::Search; + } + this.push_text(" "); + cx.notify(); + } + _ => { + // 无修饰键时把可打印字符追加到当前字段。 + if !event.keystroke.modifiers.control + && !event.keystroke.modifiers.alt + && !event.keystroke.modifiers.platform + { + let mut changed = false; + if let Some(ch) = &event.keystroke.key_char { + if this.step == ProjectDialogStep::Source { + this.active_field = ActiveField::Search; + } + this.push_text(ch); + changed = true; + } else if key.chars().count() == 1 { + if this.step == ProjectDialogStep::Source { + this.active_field = ActiveField::Search; + } + this.push_text(key); + changed = true; + } + if changed { + cx.notify(); + } + } + } + } + })) + .on_mouse_down( + gpui_kit::MouseButton::Left, + cx.listener(|this, _event, _window, cx| { + if this.step != ProjectDialogStep::Creating { + cx.emit(ProjectDialogEvent::Close); + } + }), + ) + .child( + v_flex() + .id("project-dialog-card") + .w(px(560.0)) + .h(px(480.0)) + .bg(crate::theme::ThemeColors::surface()) + .border_1() + .border_color(crate::theme::ThemeColors::border()) + .rounded_lg() + .shadow_lg() + .overflow_hidden() + .on_mouse_down( + gpui_kit::MouseButton::Left, + cx.listener(|_this, _event, _window, cx| { + // 卡片内点击不冒泡到遮罩,避免误关闭。 + cx.stop_propagation(); + }), + ) + .child(self.render_header(is_clone, is_creating, cx)) + .child(match self.step { + ProjectDialogStep::Source => self.render_source_body(cx).into_any_element(), + ProjectDialogStep::Details => { + self.render_details_body(is_clone, cx).into_any_element() + } + ProjectDialogStep::Creating => { + self.render_creating_body(is_clone, cx).into_any_element() + } + }) + .when(self.step == ProjectDialogStep::Details, |card| { + card.child(self.render_footer(is_clone, cx)) + }), + ) + } +} + +/// 头部 / 各步骤主体 / 底部渲染。 +impl ProjectDialog { + /// 头部:返回按钮 + 图标标题 + 关闭按钮(创建中隐藏可关闭项)。 + fn render_header( + &self, + is_clone: bool, + is_creating: bool, + cx: &mut Context, + ) -> impl IntoElement { + let (icon, title) = if is_creating { + ( + if is_clone { + IconName::GitBranch + } else { + self.source.icon() + }, + crate::i18n::menu_text( + cx, + if is_clone { + "newProject.cloningRepository" + } else { + "newProject.creatingProject" + }, + ) + .to_string(), + ) + } else { + match (self.mode, self.step) { + (ProjectDialogMode::New, ProjectDialogStep::Source) => ( + IconName::Folder, + crate::i18n::menu_text(cx, "welcome.newProject").to_string(), + ), + (_, ProjectDialogStep::Details) if is_clone => ( + IconName::GitBranch, + crate::i18n::menu_text(cx, "welcome.cloneRepository").to_string(), + ), + _ => (self.source.icon(), self.source.label(cx)), + } + }; + // 返回行为:来源页/Clone 详情关闭对话框,New 详情返回来源页。 + let (back_tooltip, back_to_source) = match (self.mode, self.step) { + (ProjectDialogMode::New, ProjectDialogStep::Details) => ( + crate::i18n::menu_text(cx, "newProject.backToStarters").to_string(), + true, + ), + _ => ( + crate::i18n::menu_text(cx, "welcome.backToProjects").to_string(), + false, + ), + }; + + h_flex() + .h(px(44.0)) + .w_full() + .items_center() + .gap_2() + .px_3() + .border_b_1() + .border_color(crate::theme::ThemeColors::border()) + .bg(crate::theme::ThemeColors::surface()) + .when(!is_creating, |header| { + header.child( + Button::new("project-dialog-header-back") + .small() + .ghost() + .icon(IconName::ArrowLeft) + .tooltip(back_tooltip) + .on_click(cx.listener(move |this, _event, _window, cx| { + if back_to_source { + this.back_to_source(cx); + } else { + cx.emit(ProjectDialogEvent::Close); + } + })), + ) + }) + .child( + Icon::new(icon) + .size(px(16.0)) + .text_color(crate::theme::ThemeColors::primary()), + ) + .child( + div() + .flex_1() + .truncate() + .text_sm() + .font_weight(FontWeight::BOLD) + .text_color(crate::theme::ThemeColors::foreground()) + .child(title), + ) + .when(!is_creating, |header| { + header.child( + Button::new("project-dialog-close") + .small() + .ghost() + .icon(IconName::Close) + .tooltip(crate::i18n::menu_text(cx, "ui.close")) + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(ProjectDialogEvent::Close); + })), + ) + }) + } + + /// 来源选择主体:搜索行 + 来源行列表(空态显示 `noStarters`)。 + fn render_source_body(&self, cx: &mut Context) -> impl IntoElement { + let ids = self.filtered_ids(cx); + let current = self.selected_index.min(ids.len().saturating_sub(1)); + let query = self.query.clone(); + let placeholder = crate::i18n::menu_text(cx, "welcome.chooseHowToStart").to_string(); + + v_flex() + .flex_1() + .w_full() + .min_h_0() + .overflow_hidden() + .child( + h_flex() + .id("project-dialog-search") + .h(px(44.0)) + .w_full() + .flex_shrink_0() + .items_center() + .gap_2p5() + .px_4() + .border_b_1() + .border_color(crate::theme::ThemeColors::border()) + .cursor_pointer() + .child( + Icon::new(IconName::Search) + .size(px(15.0)) + .text_color(crate::theme::ThemeColors::primary()), + ) + .child( + h_flex() + .flex_1() + .items_center() + .gap_1() + .child( + div() + .text_sm() + .text_color(if query.is_empty() { + crate::theme::ThemeColors::subtle_foreground() + } else { + crate::theme::ThemeColors::foreground() + }) + .child(if query.is_empty() { + placeholder + } else { + query.clone() + }), + ) + .child( + div() + .w(px(2.0)) + .h(px(16.0)) + .bg(crate::theme::ThemeColors::primary()), + ), + ) + .on_click(cx.listener(|this, _event, window, cx| { + this.active_field = ActiveField::Search; + window.focus(&this.focus_handle, cx); + cx.notify(); + })), + ) + .child( + div() + .flex_1() + .w_full() + .overflow_y_scrollbar() + .py_1() + .when(ids.is_empty(), |list| { + let empty = crate::i18n::menu_text(cx, "welcome.noStarters") + .replace("{query}", &self.query); + list.child( + div() + .w_full() + .py_8() + .text_center() + .text_sm() + .text_color(crate::theme::ThemeColors::subtle_foreground()) + .child(empty), + ) + }) + .children(ids.into_iter().enumerate().map(|(position, id)| { + let is_selected = position == current; + let label = id.label(cx); + let description = + crate::i18n::menu_text(cx, id.description_key()).to_string(); + let badge = crate::i18n::menu_text(cx, id.badge_key()).to_string(); + let icon = id.icon(); + h_flex() + .id(("project-dialog-source", position)) + .h(px(52.0)) + .w_full() + .mx_2() + .px_2p5() + .items_center() + .gap_2p5() + .rounded_md() + .cursor_pointer() + .when(is_selected, |row| { + row.bg(crate::theme::ThemeColors::selected()) + }) + .when(!is_selected, |row| { + row.hover(|h| h.bg(crate::theme::ThemeColors::accent())) + }) + .child(Icon::new(icon).size(px(16.0)).text_color(if is_selected { + crate::theme::ThemeColors::primary() + } else { + crate::theme::ThemeColors::muted_foreground() + })) + .child( + v_flex() + .flex_1() + .min_w_0() + .child( + div() + .text_sm() + .font_weight(FontWeight::MEDIUM) + .text_color(crate::theme::ThemeColors::foreground()) + .child(label), + ) + .child( + div() + .truncate() + .text_xs() + .text_color( + crate::theme::ThemeColors::subtle_foreground(), + ) + .child(description), + ), + ) + .child(source_badge(badge)) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.choose_source(id, cx); + })) + .into_any_element() + })), + ) + } + + /// 详情表单主体:URL(仅 Clone)+ 名称 + 位置 + 包管理器 + 目标预览 + 错误。 + fn render_details_body(&self, is_clone: bool, cx: &mut Context) -> impl IntoElement { + let name_error = self + .name_error_key() + .filter(|_| !self.project_name.is_empty()) + .map(|key| crate::i18n::menu_text(cx, key).to_string()); + let dest = self.destination_path(); + let show_dest = !self.location_path.trim().is_empty(); + let error = self.error_message.clone(); + let is_template = !is_clone && !matches!(self.source, NewProjectSource::Empty); + + div() + .flex_1() + .w_full() + .min_h_0() + .overflow_y_scrollbar() + .child( + v_flex() + .w_full() + .gap_3() + .p_4() + .when(is_clone, |body| { + body.child( + self.render_text_field( + "project-dialog-field-url", + crate::i18n::menu_text(cx, "newProject.repositoryUrl").to_string(), + self.repository_url.clone(), + "https://github.com/owner/repository.git".to_string(), + Some( + crate::i18n::menu_text( + cx, + "newProject.repositoryUrlDescription", + ) + .to_string(), + ), + None, + ActiveField::RepositoryUrl, + cx, + ), + ) + }) + .child(self.render_text_field( + "project-dialog-field-name", + crate::i18n::menu_text(cx, "newProject.projectName").to_string(), + self.project_name.clone(), + if is_clone { + "repository".to_string() + } else { + "my-project".to_string() + }, + None, + name_error, + ActiveField::ProjectName, + cx, + )) + .child(self.render_location_row(cx)) + .when(is_template, |body| { + body.child(self.render_package_manager_row(cx)) + }) + .when(show_dest, |body| { + body.child( + v_flex() + .w_full() + .rounded_md() + .border_1() + .border_color(crate::theme::ThemeColors::border()) + .bg(crate::theme::ThemeColors::surface()) + .overflow_hidden() + .child( + div() + .px_3() + .py_2() + .border_b_1() + .border_color(crate::theme::ThemeColors::border()) + .text_xs() + .font_weight(FontWeight::MEDIUM) + .text_color(crate::theme::ThemeColors::subtle_foreground()) + .child( + crate::i18n::menu_text( + cx, + "newProject.projectLocation", + ) + .to_string(), + ), + ) + .child( + div() + .px_3() + .py_2() + .font_family("monospace") + .text_xs() + .text_color(crate::theme::ThemeColors::foreground()) + .child(dest), + ), + ) + }) + .when_some(error, |body, message| { + body.child( + div() + .text_xs() + .text_color(crate::theme::ThemeColors::accent_red()) + .child(message), + ) + }), + ) + } + + /// 单行文本字段:标签 + 只读展示行(点击聚焦)+ 描述/错误。 + #[allow(clippy::too_many_arguments)] + fn render_text_field( + &self, + id: &'static str, + label: String, + value: String, + placeholder: String, + description: Option, + error: Option, + field: ActiveField, + cx: &mut Context, + ) -> impl IntoElement { + let is_active = self.active_field == field; + v_flex() + .w_full() + .gap_1p5() + .child( + div() + .text_xs() + .font_weight(FontWeight::MEDIUM) + .text_color(crate::theme::ThemeColors::foreground()) + .child(label), + ) + .child( + h_flex() + .id(id) + .min_h(px(32.0)) + .w_full() + .items_center() + .gap_1() + .px_2p5() + .py_1p5() + .rounded_sm() + .border_1() + .cursor_pointer() + .when(is_active, |row| { + row.border_color(crate::theme::ThemeColors::primary()) + .bg(crate::theme::ThemeColors::background()) + }) + .when(!is_active, |row| { + row.border_color(crate::theme::ThemeColors::border()) + .bg(crate::theme::ThemeColors::background()) + .hover(|h| { + h.border_color(crate::theme::ThemeColors::muted_foreground()) + }) + }) + .child( + div() + .flex_1() + .truncate() + .text_sm() + .text_color(if value.is_empty() { + crate::theme::ThemeColors::subtle_foreground() + } else { + crate::theme::ThemeColors::foreground() + }) + .child(if value.is_empty() { placeholder } else { value }), + ) + .when(is_active, |row| { + row.child( + div() + .w(px(2.0)) + .h(px(16.0)) + .bg(crate::theme::ThemeColors::primary()), + ) + }) + .on_click(cx.listener(move |this, _event, window, cx| { + this.active_field = field; + window.focus(&this.focus_handle, cx); + cx.notify(); + })), + ) + .when_some(description, |this, desc| { + this.child( + div() + .text_xs() + .text_color(crate::theme::ThemeColors::subtle_foreground()) + .child(desc), + ) + }) + .when_some(error, |this, message| { + this.child( + div() + .text_xs() + .text_color(crate::theme::ThemeColors::accent_red()) + .child(message), + ) + }) + } + + /// 位置行:路径展示 + 选择父目录按钮。 + fn render_location_row(&self, cx: &mut Context) -> impl IntoElement { + let is_active = self.active_field == ActiveField::Location; + let value = self.location_path.clone(); + let placeholder = crate::i18n::menu_text(cx, "newProject.chooseParentFolder").to_string(); + v_flex() + .w_full() + .gap_1p5() + .child( + div() + .text_xs() + .font_weight(FontWeight::MEDIUM) + .text_color(crate::theme::ThemeColors::foreground()) + .child(crate::i18n::menu_text(cx, "newProject.location").to_string()), + ) + .child( + h_flex() + .w_full() + .items_center() + .gap_2() + .child( + h_flex() + .id("project-dialog-field-location") + .flex_1() + .min_w_0() + .min_h(px(32.0)) + .items_center() + .px_2p5() + .py_1p5() + .rounded_sm() + .border_1() + .cursor_pointer() + .font_family("monospace") + .when(is_active, |row| { + row.border_color(crate::theme::ThemeColors::primary()) + .bg(crate::theme::ThemeColors::background()) + }) + .when(!is_active, |row| { + row.border_color(crate::theme::ThemeColors::border()) + .bg(crate::theme::ThemeColors::background()) + .hover(|h| { + h.border_color(crate::theme::ThemeColors::muted_foreground()) + }) + }) + .child( + div() + .flex_1() + .truncate() + .text_sm() + .text_color(if value.is_empty() { + crate::theme::ThemeColors::subtle_foreground() + } else { + crate::theme::ThemeColors::foreground() + }) + .child(if value.is_empty() { placeholder } else { value }), + ) + .on_click(cx.listener(|this, _event, window, cx| { + this.active_field = ActiveField::Location; + window.focus(&this.focus_handle, cx); + cx.notify(); + })), + ) + .child( + Button::new("project-dialog-browse") + .small() + .ghost() + .icon(IconName::FolderOpen) + .label(crate::i18n::menu_text(cx, "newProject.chooseParentFolder")) + .tooltip( + crate::i18n::menu_text(cx, "newProject.chooseProjectLocation") + .to_string(), + ) + .on_click(cx.listener(|this, _event, _window, cx| { + this.choose_location(cx); + })), + ), + ) + } + + /// 包管理器行:标签 + 描述 + 循环切换按钮(仅模板来源)。 + fn render_package_manager_row(&self, cx: &mut Context) -> impl IntoElement { + v_flex() + .w_full() + .gap_1p5() + .child( + div() + .text_xs() + .font_weight(FontWeight::MEDIUM) + .text_color(crate::theme::ThemeColors::foreground()) + .child(crate::i18n::menu_text(cx, "newProject.packageManager").to_string()), + ) + .child( + h_flex() + .w_full() + .min_h(px(32.0)) + .items_center() + .justify_between() + .gap_2() + .child( + div() + .flex_1() + .text_xs() + .text_color(crate::theme::ThemeColors::subtle_foreground()) + .child( + crate::i18n::menu_text(cx, "newProject.packageManagerDescription") + .to_string(), + ), + ) + .child( + Button::new("project-dialog-pm") + .small() + .ghost() + .label(self.package_manager.label()) + .tooltip( + crate::i18n::menu_text(cx, "newProject.packageManager").to_string(), + ) + .on_click(cx.listener(|this, _event, _window, cx| { + this.cycle_package_manager(cx); + })), + ), + ) + } + + /// 创建中主体:进度文案 + 目标路径(禁用关闭)。 + fn render_creating_body(&self, is_clone: bool, cx: &mut Context) -> impl IntoElement { + let status = crate::i18n::menu_text( + cx, + if is_clone { + "newProject.cloningRepositoryStatus" + } else { + "newProject.preparingProject" + }, + ) + .to_string(); + v_flex() + .flex_1() + .w_full() + .items_center() + .justify_center() + .gap_2() + .p_6() + .child( + div() + .text_sm() + .text_color(crate::theme::ThemeColors::foreground()) + .child(format!("{status}...")), + ) + .child( + div() + .w_full() + .text_center() + .truncate() + .font_family("monospace") + .text_xs() + .text_color(crate::theme::ThemeColors::subtle_foreground()) + .child(self.destination_path()), + ) + } + + /// 底部:返回 + 创建/克隆按钮(Clone 模式无返回)。 + fn render_footer(&self, is_clone: bool, cx: &mut Context) -> impl IntoElement { + let can_create = self.can_create(); + let create_label = if is_clone { + crate::i18n::menu_text(cx, "welcome.cloneRepository").to_string() + } else { + match self.source { + NewProjectSource::Empty => { + crate::i18n::menu_text(cx, "welcome.emptyProject").to_string() + } + _ => crate::i18n::menu_text(cx, "welcome.newProject").to_string(), + } + }; + let create_icon = if is_clone { + IconName::GitBranch + } else { + match self.source { + NewProjectSource::Empty => IconName::FilePlus, + _ => IconName::Package, + } + }; + + h_flex() + .h(px(44.0)) + .w_full() + .flex_shrink_0() + .items_center() + .gap_2() + .px_3() + .border_t_1() + .border_color(crate::theme::ThemeColors::border()) + .bg(crate::theme::ThemeColors::surface()) + .when(!is_clone, |footer| { + footer.child( + Button::new("project-dialog-back") + .small() + .ghost() + .icon(IconName::ArrowLeft) + .label(crate::i18n::menu_text(cx, "newProject.backToStarters")) + .on_click(cx.listener(|this, _event, _window, cx| { + this.back_to_source(cx); + })), + ) + }) + .child(div().flex_1()) + .child( + Button::new("project-dialog-create") + .small() + .primary() + .icon(create_icon) + .label(create_label) + .disabled(!can_create) + .on_click(cx.listener(|this, _event, _window, cx| { + this.begin_create(cx); + })), + ) + } +} + +/// 来源徽标(对齐 quick_open 的徽标样式)。 +fn source_badge(label: String) -> AnyElement { + div() + .px_1p5() + .py(px(1.0)) + .rounded_sm() + .bg(crate::theme::ThemeColors::accent()) + .border_1() + .border_color(crate::theme::ThemeColors::border()) + .text_xs() + .text_color(crate::theme::ThemeColors::muted_foreground()) + .child(label) + .into_any_element() +} diff --git a/linux/src/workbench/quick_open.rs b/linux/src/workbench/quick_open.rs new file mode 100644 index 000000000..123e9ca2d --- /dev/null +++ b/linux/src/workbench/quick_open.rs @@ -0,0 +1,527 @@ +//! 工作台快速打开(Quick Open)模态视图。 +//! +//! 复刻 Tauri 端 `features/quick-open` 的三模式弹层:默认按文件路径筛选, +//! 以 `@` 开头切换到当前文件符号、`#` 开头切换到工作区符号。本模块只维护 +//! 查询、分组与选中态,确认后通过 [`QuickOpenEvent::OpenFile`] 把路径交给上层, +//! 不接入真实文件索引与 LSP 符号查询。 + +use gpui_kit::assets::IconName; +use gpui_kit::component::scroll::ScrollableElement as _; +use gpui_kit::component::{h_flex, v_flex, Icon}; +use gpui_kit::prelude::FluentBuilder as _; +use gpui_kit::{ + div, px, rgba, AnyElement, Context, EventEmitter, FocusHandle, FontWeight, + InteractiveElement as _, IntoElement, KeyDownEvent, ParentElement as _, Render, + StatefulInteractiveElement as _, Styled as _, Window, +}; + +use crate::theme::ThemeColors; + +/// 快速打开的工作模式,由查询串前缀决定。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum QuickOpenMode { + /// 默认模式:按文件路径筛选。 + File, + /// `@` 前缀:当前文件符号。 + Symbol, + /// `#` 前缀:工作区符号。 + WorkspaceSymbol, +} + +impl QuickOpenMode { + /// 输入占位文案键(与 Tauri `quickOpen.search*` 对齐)。 + fn placeholder_key(self) -> &'static str { + match self { + QuickOpenMode::File => "quickOpen.searchFiles", + QuickOpenMode::Symbol => "quickOpen.searchSymbols", + QuickOpenMode::WorkspaceSymbol => "quickOpen.searchWorkspaceSymbols", + } + } + + /// 列表为空时展示的提示文案(与 Tauri `quickOpen.no*` 对齐)。 + fn empty_label(self, cx: &gpui_kit::App) -> &'static str { + match self { + QuickOpenMode::File => crate::i18n::menu_text(cx, "quickOpen.noMatch"), + QuickOpenMode::Symbol | QuickOpenMode::WorkspaceSymbol => { + crate::i18n::menu_text(cx, "quickOpen.noSymbols") + } + } + } +} + +/// 快速打开对外事件。 +#[derive(Debug, Clone)] +pub enum QuickOpenEvent { + /// 打开选中的文件或跳转到选中的符号,携带路径。 + OpenFile(String), + /// 请求关闭快速打开。 + Close, +} + +/// 列表渲染行:分组标题或文件项(携带该项在 `filtered` 中的位置)。 +enum QuickOpenRow { + File(usize, String), +} + +/// 居中的快速打开模态,宽 704、高 420。 +pub struct QuickOpenModal { + pub query: String, + pub mode: QuickOpenMode, + pub files: Vec, + /// 命中筛选的文件路径,保持已打开、最近、其他三分组的扁平顺序 + /// (对齐 Tauri `openBufferFiles`、`recentFilesInResults`、`otherFiles`)。 + pub filtered: Vec, + pub selected_index: usize, + pub focus_handle: FocusHandle, + /// 已打开的编辑器标签页路径,用于优先分组展示(对齐 Tauri `openBufferFiles`)。 + pub open_files: Vec, + /// 最近打开的文件,用于优先分组展示。 + pub recent: Vec, +} + +impl EventEmitter for QuickOpenModal {} + +impl QuickOpenModal { + pub fn new(cx: &mut Context) -> Self { + Self { + query: String::new(), + mode: QuickOpenMode::File, + files: Vec::new(), + filtered: Vec::new(), + selected_index: 0, + focus_handle: cx.focus_handle(), + open_files: Vec::new(), + recent: Vec::new(), + } + } + + /// 替换候选文件列表并重算筛选结果。 + pub fn set_files(&mut self, files: Vec, cx: &mut Context) { + self.files = files; + self.recompute_filtered(); + cx.notify(); + } + + /// 同步已打开的编辑器标签页路径(打开分组置顶,对齐 Tauri `openBufferFiles`)。 + pub fn set_open_files(&mut self, open: Vec, cx: &mut Context) { + self.open_files = open; + self.recompute_filtered(); + cx.notify(); + } + + /// 记录最近打开的文件(去重、前置、上限 20,对齐 Tauri `recentFilesInResults`)。 + pub fn push_recent(&mut self, path: String, cx: &mut Context) { + self.recent.retain(|r| *r != path); + self.recent.insert(0, path); + self.recent.truncate(20); + self.recompute_filtered(); + cx.notify(); + } + + /// 复位到初始状态:清空查询与选中,回到 File 模式。 + pub fn reset(&mut self, cx: &mut Context) { + self.query.clear(); + self.mode = QuickOpenMode::File; + self.selected_index = 0; + self.recompute_filtered(); + cx.notify(); + } + + /// 设置查询串并同步模式(`@`/`#` 前缀)与筛选结果。 + #[allow(dead_code)] + pub fn set_query(&mut self, q: impl Into, cx: &mut Context) { + self.query = q.into(); + self.mode = mode_for_query(&self.query); + self.selected_index = 0; + self.recompute_filtered(); + cx.notify(); + } + + /// 在当前命中列表内移动选中项,`delta` 为正向下、为负向上,越界即夹紧。 + pub fn move_selection(&mut self, delta: i32, cx: &mut Context) { + let total = self.filtered.len(); + if total == 0 { + self.selected_index = 0; + } else { + let current = self.selected_index.min(total - 1) as i32; + self.selected_index = (current + delta).clamp(0, total as i32 - 1) as usize; + } + cx.notify(); + } + + /// 当前有效选中下标(对空列表安全)。 + fn current_index(&self) -> usize { + if self.filtered.is_empty() { + 0 + } else { + self.selected_index.min(self.filtered.len() - 1) + } + } + + /// 当前用于匹配的查询串:符号模式下忽略前缀字符。 + fn effective_query(&self) -> String { + if self.mode == QuickOpenMode::File { + self.query.trim().to_lowercase() + } else { + self.query + .trim_start_matches(|c| c == '@' || c == '#') + .trim() + .to_lowercase() + } + } + + /// 按有效查询串重算命中列表:已打开优先、最近其次、其余最后 + /// (对齐 Tauri `useFileSearch` 的三段分组与全局选中下标)。 + fn recompute_filtered(&mut self) { + let q = self.effective_query(); + let matches = |path: &str| q.is_empty() || path.to_lowercase().contains(&q); + let mut seen = std::collections::HashSet::new(); + let mut ordered = Vec::new(); + // 已打开分组:保持传入的标签页顺序。 + for path in &self.open_files { + if matches(path) && seen.insert(path.clone()) { + ordered.push(path.clone()); + } + } + // 最近分组:保持最近优先顺序,排除已计入的已打开项。 + for path in &self.recent { + if self.files.iter().any(|f| f == path) || self.open_files.iter().any(|f| f == path) { + if matches(path) && seen.insert(path.clone()) { + ordered.push(path.clone()); + } + } + } + // 其余文件:保持 `files` 的原始顺序。 + for path in &self.files { + if matches(path) && seen.insert(path.clone()) { + ordered.push(path.clone()); + } + } + self.filtered = ordered; + if self.selected_index >= self.filtered.len() { + self.selected_index = self.filtered.len().saturating_sub(1); + } + } + + /// 把命中列表展开为行序列(Tauri 无分组头,保持 recent 优先的扁平顺序)。 + fn build_rows(&self) -> Vec { + self.filtered + .iter() + .enumerate() + .map(|(position, path)| QuickOpenRow::File(position, path.clone())) + .collect() + } + + /// 打开命中列表第 `position` 项。 + fn open_at(&self, position: usize, cx: &mut Context) { + if let Some(path) = self.filtered.get(position) { + cx.emit(QuickOpenEvent::OpenFile(path.clone())); + } + } +} + +/// 根据查询串前缀解析模式。 +fn mode_for_query(query: &str) -> QuickOpenMode { + if query.starts_with('@') { + QuickOpenMode::Symbol + } else if query.starts_with('#') { + QuickOpenMode::WorkspaceSymbol + } else { + QuickOpenMode::File + } +} + +impl Render for QuickOpenModal { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + // 请求聚焦以接收按键输入 + window.focus(&self.focus_handle, cx); + + let rows = self.build_rows(); + let current_index = self.current_index(); + let mode = self.mode; + let is_dark = { + let s = crate::settings::get(cx); + !crate::theme::ThemePalette::is_light(&crate::settings::resolved_theme_id(s, false)) + }; + + // 全屏半透明遮罩:点击空白处关闭 + div() + .id("quick-open-backdrop") + .track_focus(&self.focus_handle) + .absolute() + .inset_0() + .bg(rgba(0x00000088)) + .flex() + .items_center() + .justify_center() + .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { + let key = event.keystroke.key.as_str(); + match key { + "escape" => { + cx.emit(QuickOpenEvent::Close); + } + "up" | "arrowup" => this.move_selection(-1, cx), + "down" | "arrowdown" => this.move_selection(1, cx), + "enter" => { + if !this.filtered.is_empty() { + let idx = this.current_index(); + this.open_at(idx, cx); + } + } + "backspace" => { + this.query.pop(); + this.mode = mode_for_query(&this.query); + this.selected_index = 0; + this.recompute_filtered(); + cx.notify(); + } + "space" => { + this.query.push(' '); + this.selected_index = 0; + this.recompute_filtered(); + cx.notify(); + } + _ => { + // 无修饰键时把可打印字符追加到查询串,并随前缀切换模式 + if !event.keystroke.modifiers.control + && !event.keystroke.modifiers.alt + && !event.keystroke.modifiers.platform + { + let mut changed = false; + if let Some(ch) = &event.keystroke.key_char { + this.query.push_str(ch); + changed = true; + } else if key.chars().count() == 1 { + this.query.push_str(key); + changed = true; + } + if changed { + this.mode = mode_for_query(&this.query); + this.selected_index = 0; + this.recompute_filtered(); + cx.notify(); + } + } + } + } + })) + .on_mouse_down( + gpui_kit::MouseButton::Left, + cx.listener(|_this, _event, _window, cx| { + cx.emit(QuickOpenEvent::Close); + }), + ) + .child( + v_flex() + .id("quick-open-card") + .w(px(704.0)) + .h(px(420.0)) + .bg(ThemeColors::surface()) + .border_1() + .border_color(ThemeColors::border()) + .rounded_lg() + .shadow_lg() + .overflow_hidden() + .on_mouse_down( + gpui_kit::MouseButton::Left, + cx.listener(|_this, _event, _window, cx| { + // 卡片内点击不冒泡到遮罩,避免误关闭 + cx.stop_propagation(); + }), + ) + .child( + // 1. 顶部输入行:搜索图标 + 查询串/占位 + Esc 徽标 + h_flex() + .h(px(52.0)) + .w_full() + .items_center() + .gap_2p5() + .px_4() + .border_b_1() + .border_color(ThemeColors::border()) + .child( + Icon::new(IconName::Search) + .size(px(16.0)) + .text_color(ThemeColors::primary()), + ) + .child( + h_flex() + .flex_1() + .items_center() + .gap_1() + .child( + div() + .text_sm() + .text_color(if self.query.is_empty() { + ThemeColors::subtle_foreground() + } else { + ThemeColors::foreground() + }) + .child(if self.query.is_empty() { + crate::i18n::menu_text(cx, mode.placeholder_key()) + .to_string() + } else { + self.query.clone() + }), + ) + .child( + // 静态光标条,提示可输入 + div().w(px(2.0)).h(px(16.0)).bg(ThemeColors::primary()), + ), + ) + .child(count_badge( + self.filtered.len(), + self.files.len(), + !self.query.is_empty(), + cx, + )) + .child(shortcut_badge("Esc")), + ) + .child( + // 2. 结果列表:已打开优先、最近其次、其余最后,路径分段着色 + div() + .flex_1() + .w_full() + .overflow_y_scrollbar() + .py_1() + .when(rows.is_empty(), |list| { + list.child( + div() + .w_full() + .py_8() + .text_center() + .text_sm() + .text_color(ThemeColors::subtle_foreground()) + .child(mode.empty_label(cx)), + ) + }) + .children(rows.into_iter().map(|row| match row { + QuickOpenRow::File(position, path) => { + let is_selected = position == current_index; + let is_recent = self.recent.iter().any(|r| *r == path); + let (dir, name) = split_path(&path); + + h_flex() + .id(("quick-open-item", position)) + .h(px(32.0)) + .w_full() + .mx_2() + .px_2p5() + .items_center() + .gap_2() + .rounded_md() + .cursor_pointer() + .when(is_selected, |row| row.bg(ThemeColors::selected())) + .when(!is_selected, |row| { + row.hover(|h| h.bg(ThemeColors::accent())) + }) + .child( + if let Some(arc) = + crate::workbench::file_icon::file_image( + &name, is_dark, + ) + { + div() + .size(px(14.0)) + .child( + gpui_kit::img( + gpui_kit::ImageSource::Image(arc), + ) + .size_full(), + ) + .into_any_element() + } else { + Icon::new(IconName::FileText) + .size(px(14.0)) + .text_color(if is_selected { + ThemeColors::primary() + } else { + ThemeColors::muted_foreground() + }) + .into_any_element() + }, + ) + .child( + div() + .flex_1() + .text_sm() + .font_weight(FontWeight::MEDIUM) + .text_color(ThemeColors::foreground()) + .child(name), + ) + .when(!dir.is_empty(), |row| { + row.child( + div() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child(dir), + ) + }) + .when(is_recent, |row| { + row.child( + Icon::new(IconName::Clock) + .size(px(13.0)) + .text_color(ThemeColors::subtle_foreground()), + ) + }) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.open_at(position, cx); + })) + .into_any_element() + } + })), + ), + ) + } +} + +/// 把路径拆成 `(目录, 文件名)`,无目录时目录为空串。 +fn split_path(path: &str) -> (String, String) { + match path.rsplit_once('/') { + Some((dir, name)) => (dir.to_string(), name.to_string()), + None => (String::new(), path.to_string()), + } +} + +/// 快捷键/键位徽标。 +fn shortcut_badge(label: &str) -> AnyElement { + div() + .px_1p5() + .py(px(1.0)) + .rounded_sm() + .bg(ThemeColors::accent()) + .border_1() + .border_color(ThemeColors::border()) + .text_xs() + .text_color(ThemeColors::muted_foreground()) + .child(label.to_string()) + .into_any_element() +} + +/// 文件计数徽标:有查询时 `命中 / 总数`,无查询时 `N 个文件` +/// (对齐 Tauri `FileCountBadge`)。 +fn count_badge(matched: usize, total: usize, has_query: bool, cx: &gpui_kit::App) -> AnyElement { + if total == 0 { + return div().into_any_element(); + } + let text = if has_query { + format!("{matched} / {total}") + } else if total == 1 { + crate::i18n::menu_text(cx, "quickOpen.fileCountOne").to_string() + } else { + crate::i18n::menu_text(cx, "quickOpen.filesCount") + .to_string() + .replace("{count}", &total.to_string()) + }; + div() + .px_1p5() + .py(px(1.0)) + .rounded_sm() + .bg(ThemeColors::accent()) + .border_1() + .border_color(ThemeColors::border()) + .text_xs() + .text_color(ThemeColors::muted_foreground()) + .child(text) + .into_any_element() +} diff --git a/linux/src/workbench/run/config.rs b/linux/src/workbench/run/config.rs new file mode 100644 index 000000000..3d1bc0ec1 --- /dev/null +++ b/linux/src/workbench/run/config.rs @@ -0,0 +1,1105 @@ +//! Run 配置文档、工具链读取和 Linux 启动计划的纯适配。 +//! +//! 这里不判断 Java 入口是否存在,也不从源码文本推导项目模型;这些事实由 +//! Core/JDT 生成。Linux 只保存 Core 返回的文档,并把平台路径和工具链选择 +//! 解析成 `std::process::Command` 所需的形状。 + +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; + +use serde_json::{json, Value}; + +const SIDECAR_VERSION: u32 = 1; +const RUN_VERSION: u32 = 2; +const MAX_JAVA_SOURCES: usize = 8_000; +const MAX_JAVA_WALK_DEPTH: usize = 32; +const LITHE_GITIGNORE_ENTRIES: &[&str] = &[ + "run/local.json", + "toolchains/local.json", + "run/classes/", + "**/*.tmp", +]; + +static TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0); + +/// Core 解析后的一个有效运行配置。 +#[allow(dead_code)] +#[derive(Debug, Clone)] +pub struct RunConfigItem { + pub id: String, + pub name: String, + pub provider: String, + pub kind: String, + pub detail: String, + pub execution: String, + pub category: String, + pub cwd: String, + pub args: Vec, + pub env: BTreeMap, + pub toolchains: BTreeMap, + pub extensions: Value, + pub main_class: Option, + pub source: Option, + pub module: Option, + pub raw: Value, +} + +impl RunConfigItem { + /// 从 `runConfig.resolve` 的有效配置项构造展示模型。 + pub fn from_value(value: &Value) -> Option { + let object = value.as_object()?; + let id = object.get("id")?.as_str()?.to_string(); + let name = object + .get("name") + .and_then(Value::as_str) + .unwrap_or(&id) + .to_string(); + let provider = object + .get("provider") + .and_then(Value::as_str) + .unwrap_or("") + .to_string(); + let command = object.get("command").and_then(Value::as_str); + let extensions = object + .get("extensions") + .cloned() + .unwrap_or_else(|| json!({})); + let toolchains = string_map(object.get("toolchains")); + let args = string_array(object.get("args")).unwrap_or_default(); + let env = string_map(object.get("env")); + let module = extensions + .get("maven") + .and_then(|maven| maven.get("module")) + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + .map(str::to_string); + let main_class = extensions + .get("maven") + .and_then(|maven| maven.get("mainClass")) + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + .map(str::to_string); + let source = extensions + .get("java") + .and_then(|java| java.get("source")) + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + .map(str::to_string); + let kind = if provider.starts_with("java.") { + "java".to_string() + } else if provider.contains("maven") || toolchains.contains_key("maven") { + "maven".to_string() + } else if command.is_some_and(|value| { + let lower = value.to_ascii_lowercase(); + lower.contains("npm") + || lower.contains("node") + || lower.contains("pnpm") + || lower.contains("yarn") + }) { + "npm".to_string() + } else { + "process".to_string() + }; + let detail = main_class + .clone() + .or_else(|| module.clone()) + .or_else(|| source.clone()) + .or_else(|| { + let value = args.join(" "); + (!value.is_empty()).then_some(value) + }) + .or_else(|| command.map(str::to_string)) + .unwrap_or_else(|| provider.clone()); + Some(Self { + id, + name, + provider, + kind, + detail, + execution: object + .get("execution") + .and_then(Value::as_str) + .unwrap_or("application") + .to_string(), + category: object + .get("category") + .and_then(Value::as_str) + .unwrap_or("project") + .to_string(), + cwd: object + .get("cwd") + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + .unwrap_or(".") + .to_string(), + args, + env, + toolchains, + extensions, + main_class, + source, + module, + raw: value.clone(), + }) + } + + pub fn uses_maven(&self) -> bool { + self.toolchains.contains_key("maven") + || self.provider.contains("maven") + || (self.provider == "java.main" + && self + .extensions + .get("maven") + .and_then(|value| value.get("module")) + .is_some()) + } + + pub fn java_home_path(&self) -> Option<&str> { + self.extensions + .get("java") + .and_then(|java| java.get("homePath")) + .and_then(Value::as_str) + .filter(|value| !value.trim().is_empty()) + } + + pub fn maven_executable_path(&self) -> Option<&str> { + self.extensions + .get("java") + .and_then(|java| java.get("mavenExecutablePath")) + .and_then(Value::as_str) + .filter(|value| !value.trim().is_empty()) + } + + pub fn maven_java_home_path(&self) -> Option<&str> { + self.extensions + .get("java") + .and_then(|java| java.get("mavenJavaHomePath")) + .and_then(Value::as_str) + .filter(|value| !value.trim().is_empty()) + } +} + +/// 判断一次异步结果是否仍属于当前请求;旧 reload/execution token 必须被拒绝。 +pub fn sequence_is_current(current: u64, expected: u64) -> bool { + current == expected +} + +/// Core resolve 返回的项目快照。 +#[derive(Debug, Clone)] +pub struct ResolvedRunProject { + pub configurations: Vec, + pub default_configuration_id: Option, + pub diagnostics: Vec, +} + +/// 解析 Core 的有效配置数组,保留稳定顺序并提取可展示错误。 +pub fn parse_resolved_configurations(value: &Value) -> Result { + let configurations = value + .get("configurations") + .and_then(Value::as_array) + .ok_or_else(|| "Run configuration resolve returned no configurations array".to_string())?; + let parsed = configurations + .iter() + .filter_map(RunConfigItem::from_value) + .collect::>(); + let default_configuration_id = value + .get("defaultRunConfiguration") + .and_then(Value::as_str) + .filter(|id| !id.is_empty()) + .map(str::to_string); + let diagnostics = value + .get("diagnostics") + .and_then(Value::as_array) + .map(|items| { + items + .iter() + .filter_map(|item| { + item.get("message") + .and_then(Value::as_str) + .map(str::to_string) + .or_else(|| item.as_str().map(str::to_string)) + }) + .collect() + }) + .unwrap_or_default(); + Ok(ResolvedRunProject { + configurations: parsed, + default_configuration_id, + diagnostics, + }) +} + +/// Tauri 持久化规则中的默认配置:优先框架服务,其次非 Current File。 +pub fn default_generated_configuration_id(generated: &Value) -> Option { + let configurations = generated.get("configurations").and_then(Value::as_array)?; + let framework = configurations.iter().find(|item| { + matches!( + item.get("provider").and_then(Value::as_str), + Some("spring-boot.maven" | "quarkus.maven" | "micronaut.maven") + ) + }); + framework + .or_else(|| { + configurations + .iter() + .find(|item| item.get("id").and_then(Value::as_str) != Some("current-file")) + }) + .and_then(|item| item.get("id")) + .and_then(Value::as_str) + .map(str::to_string) +} + +/// 只列出 Java 文件路径,不读取源码、不判断 main;入口事实仍由 Core/JDT 提供。 +pub fn list_java_sources(root: &str) -> Vec { + fn walk(root: &Path, directory: &Path, depth: usize, output: &mut Vec) { + if depth > MAX_JAVA_WALK_DEPTH || output.len() >= MAX_JAVA_SOURCES { + return; + } + let Ok(entries) = fs::read_dir(directory) else { + return; + }; + let mut entries = entries.filter_map(Result::ok).collect::>(); + entries.sort_by_key(|entry| entry.file_name()); + for entry in entries { + if output.len() >= MAX_JAVA_SOURCES { + break; + } + let path = entry.path(); + let name = entry.file_name().to_string_lossy().into_owned(); + let Ok(file_type) = entry.file_type() else { + continue; + }; + if file_type.is_dir() { + if name.starts_with('.') || is_build_directory(&name) { + continue; + } + walk(root, &path, depth + 1, output); + } else if file_type.is_file() && name.to_ascii_lowercase().ends_with(".java") { + if let Ok(relative) = path.strip_prefix(root) { + output.push(relative.to_string_lossy().replace('\\', "/")); + } + } + } + } + + let root_path = Path::new(root); + let mut output = Vec::new(); + walk(root_path, root_path, 0, &mut output); + output.sort(); + output.dedup(); + output +} + +fn is_build_directory(name: &str) -> bool { + matches!( + name, + "target" + | "build" + | "out" + | "dist" + | "node_modules" + | "vendor" + | "coverage" + | "bin" + | "obj" + | ".gradle" + ) +} + +/// Linux 主机保存的机器工具链选择。 +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct ToolchainPaths { + pub java_home_path: String, + pub maven_executable_path: String, + pub maven_java_home_path: String, + pub runtime_executable_paths: BTreeMap, + pub maven_settings_path: String, + pub local_repository_path: String, +} + +/// 读取 canonical Core 形状以及旧 Linux 扁平键;canonical 值优先。 +pub fn read_toolchain_paths(root: &str) -> ToolchainPaths { + let mut result = ToolchainPaths::default(); + let local_path = Path::new(root).join(".lithe/run/local.json"); + if let Ok(text) = fs::read_to_string(local_path) { + if let Ok(document) = serde_json::from_str::(&text) { + let toolchain = document.get("toolchain"); + result.java_home_path = first_toolchain_value(toolchain, &["javaHomePath", "javaHome"]) + .or_else(|| nested_toolchain_value(toolchain, "java", "homePath")) + .unwrap_or_default(); + result.maven_executable_path = + first_toolchain_value(toolchain, &["mavenExecutablePath", "mavenExecutable"]) + .or_else(|| nested_toolchain_value(toolchain, "maven", "executablePath")) + .unwrap_or_default(); + result.maven_java_home_path = + first_toolchain_value(toolchain, &["mavenJavaHomePath", "mavenJavaHome"]) + .or_else(|| nested_toolchain_value(toolchain, "maven", "javaHomePath")) + .unwrap_or_default(); + } + } + let local_toolchains_path = Path::new(root).join(".lithe/toolchains/local.json"); + if let Ok(text) = fs::read_to_string(local_toolchains_path) { + if let Ok(document) = serde_json::from_str::(&text) { + if let Some(entries) = document.get("toolchains").and_then(Value::as_object) { + for (id, entry) in entries { + if let Some(executable) = entry + .get("executable") + .and_then(Value::as_str) + .filter(|value| !value.trim().is_empty()) + { + result + .runtime_executable_paths + .insert(id.clone(), executable.to_string()); + } + } + } + } + } + result +} + +fn first_toolchain_value(toolchain: Option<&Value>, keys: &[&str]) -> Option { + keys.iter().find_map(|key| { + toolchain + .and_then(|value| value.get(*key)) + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(str::to_string) + }) +} + +fn nested_toolchain_value(toolchain: Option<&Value>, group: &str, key: &str) -> Option { + toolchain + .and_then(|value| value.get(group)) + .and_then(|value| value.get(key)) + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(str::to_string) +} + +/// 构造 Core resolve 所需的候选;候选只描述已发现的真实路径,不执行安装探测。 +pub fn toolchain_candidates(root: &str, paths: &ToolchainPaths) -> Vec { + let mut candidates = Vec::new(); + let mut add = |id: &str, kind: &str, path: &str| { + if path.trim().is_empty() { + return; + } + let candidate = Path::new(root).join(path); + let exists = candidate.is_file() || candidate.is_dir(); + if exists { + candidates.push(json!({ + "id": id, + "type": kind, + "version": "", + "vendor": "" + })); + } + }; + add("project-jdk", "java", &paths.java_home_path); + add("project-maven", "maven", &paths.maven_executable_path); + if let Some(node) = paths.runtime_executable_paths.get("project-node") { + add("project-node", "node", node); + } + candidates +} + +/// 写回项目 local 层;保留配置覆盖和旧文档其它字段。 +pub fn write_toolchain_paths(root: &str, paths: &ToolchainPaths) -> Result<(), String> { + let path = Path::new(root).join(".lithe/run/local.json"); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).map_err(|error| error.to_string())?; + } + let mut document = match fs::read_to_string(&path) { + Ok(text) => { + let value: Value = serde_json::from_str(&text) + .map_err(|error| format!("Run local configuration is invalid JSON: {error}"))?; + if !value.is_object() { + return Err("Run local configuration must be a JSON object.".to_string()); + } + value + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + json!({"version": RUN_VERSION, "configurations": []}) + } + Err(error) => return Err(error.to_string()), + }; + if document.get("version").is_none() { + document["version"] = json!(RUN_VERSION); + } + let mut toolchain = document + .get("toolchain") + .and_then(Value::as_object) + .cloned() + .unwrap_or_default(); + for legacy in ["javaHome", "mavenExecutable", "mavenJavaHome"] { + toolchain.remove(legacy); + } + toolchain.insert("javaHomePath".to_string(), json!(paths.java_home_path)); + toolchain.insert( + "mavenExecutablePath".to_string(), + json!(paths.maven_executable_path), + ); + toolchain.insert( + "mavenJavaHomePath".to_string(), + json!(paths.maven_java_home_path), + ); + toolchain.insert( + "java".to_string(), + json!({"homePath": paths.java_home_path}), + ); + toolchain.insert( + "maven".to_string(), + json!({ + "executablePath": paths.maven_executable_path, + "javaHomePath": paths.maven_java_home_path + }), + ); + document["toolchain"] = Value::Object(toolchain); + write_json_atomically(&path, &document) +} + +/// 写 Core 生成结果及三个 `.lithe` 辅助文档;任一写入失败会恢复已写文件。 +pub fn write_generated_documents( + root: &str, + generated: &Value, + requirements: &Value, + default_configuration_id: Option<&str>, +) -> Result<(), String> { + let root_path = Path::new(root); + if !root_path.is_dir() { + return Err("The project directory is unavailable.".to_string()); + } + let lithe = root_path.join(".lithe"); + let run_dir = lithe.join("run"); + let toolchain_dir = lithe.join("toolchains"); + let generated_path = run_dir.join("generated.json"); + let requirements_path = toolchain_dir.join("requirements.json"); + let ignore_path = lithe.join(".gitignore"); + let manifest_path = lithe.join("project.json"); + + fs::create_dir_all(&run_dir).map_err(|error| error.to_string())?; + fs::create_dir_all(&toolchain_dir).map_err(|error| error.to_string())?; + + let manifest = if manifest_path.exists() { + fs::read(&manifest_path).map_err(|error| error.to_string())? + } else { + let mut value = json!({"version": SIDECAR_VERSION}); + if let Some(id) = default_configuration_id.filter(|id| !id.trim().is_empty()) { + value["defaultRunConfiguration"] = json!(id); + } + serde_json::to_vec_pretty(&value).map_err(|error| error.to_string())? + }; + let ignore = ensure_gitignore(&ignore_path)?; + let generated_bytes = + serde_json::to_vec_pretty(generated).map_err(|error| error.to_string())?; + let requirements_bytes = + serde_json::to_vec_pretty(requirements).map_err(|error| error.to_string())?; + let documents = [ + (generated_path, generated_bytes), + (requirements_path, requirements_bytes), + (ignore_path, ignore), + (manifest_path, manifest), + ]; + write_document_transaction(&documents) +} + +fn ensure_gitignore(path: &Path) -> Result, String> { + let existing = if path.is_file() { + fs::read_to_string(path).map_err(|error| error.to_string())? + } else { + String::new() + }; + let mut lines = existing.lines().map(str::to_string).collect::>(); + for entry in LITHE_GITIGNORE_ENTRIES { + if !lines.iter().any(|line| line.trim() == *entry) { + lines.push((*entry).to_string()); + } + } + Ok(format!("{}\n", lines.join("\n")).into_bytes()) +} + +fn write_document_transaction(documents: &[(PathBuf, Vec)]) -> Result<(), String> { + let snapshots = documents + .iter() + .map(|(path, _)| { + if path.exists() { + fs::read(path).map(Some).map_err(|error| error.to_string()) + } else { + Ok(None) + } + }) + .collect::, String>>()?; + let mut completed: Vec<&Path> = Vec::new(); + for (path, contents) in documents { + if let Err(error) = write_bytes_atomically(path, contents) { + let mut rollback_error = None; + for completed_path in completed.iter().rev() { + let index = documents + .iter() + .position(|(candidate, _)| candidate == completed_path) + .unwrap_or(0); + let result = match &snapshots[index] { + Some(previous) => write_bytes_atomically(completed_path, previous), + None => fs::remove_file(completed_path).or_else(|error| { + if error.kind() == std::io::ErrorKind::NotFound { + Ok(()) + } else { + Err(error.to_string()) + } + }), + }; + if let Err(error) = result { + rollback_error = Some(error); + } + } + return match rollback_error { + Some(rollback) => Err(format!("{error}; rollback failed: {rollback}")), + None => Err(error), + }; + } + completed.push(path.as_path()); + } + Ok(()) +} + +fn write_json_atomically(path: &Path, value: &Value) -> Result<(), String> { + let bytes = serde_json::to_vec_pretty(value).map_err(|error| error.to_string())?; + write_bytes_atomically(path, &bytes) +} + +fn write_bytes_atomically(path: &Path, contents: &[u8]) -> Result<(), String> { + if path.exists() { + if fs::read(path).ok().as_deref() == Some(contents) { + return Ok(()); + } + } + let file_name = path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("document.json"); + let temporary = path.with_file_name(format!( + ".{file_name}.{}.tmp", + TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed) + )); + fs::write(&temporary, contents).map_err(|error| error.to_string())?; + match fs::rename(&temporary, path) { + Ok(()) => Ok(()), + Err(error) => { + let _ = fs::remove_file(&temporary); + Err(error.to_string()) + } + } +} + +/// Core `createLaunchPlan` 的 Linux 形状。 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LaunchExecutable { + pub toolchain: Option, + pub command: Option, + pub tool: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PreLaunchStep { + pub executable: LaunchExecutable, + pub arguments: Vec, + pub classpath: Vec, + pub modulepath: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LaunchPlan { + pub executable: LaunchExecutable, + pub arguments: Vec, + pub working_directory: String, + pub environment: BTreeMap, + pub env: BTreeMap, + pub pre_launch_steps: Vec, + pub classpath: Vec, + pub modulepath: Vec, +} + +impl LaunchPlan { + pub fn from_value(value: &Value) -> Result { + let executable = value + .get("executable") + .ok_or_else(|| "Launch plan has no executable".to_string())?; + let executable = LaunchExecutable::from_value(executable)?; + let arguments = string_array(value.get("arguments")) + .ok_or_else(|| "Launch plan arguments must be an array of strings".to_string())?; + let pre_launch_steps = value + .get("preLaunchSteps") + .and_then(Value::as_array) + .map(|items| { + items + .iter() + .map(PreLaunchStep::from_value) + .collect::, _>>() + }) + .transpose()? + .unwrap_or_default(); + Ok(Self { + executable, + arguments, + working_directory: value + .get("workingDirectory") + .and_then(Value::as_str) + .filter(|cwd| !cwd.is_empty()) + .unwrap_or(".") + .to_string(), + environment: value + .get("environment") + .and_then(Value::as_object) + .map(|entries| { + entries + .iter() + .map(|(key, value)| (key.clone(), value.clone())) + .collect() + }) + .unwrap_or_default(), + env: string_map(value.get("env")), + pre_launch_steps, + classpath: string_array(value.get("classpath")).unwrap_or_default(), + modulepath: string_array(value.get("modulepath")).unwrap_or_default(), + }) + } +} + +impl LaunchExecutable { + fn from_value(value: &Value) -> Result { + let object = value + .as_object() + .ok_or_else(|| "Launch executable must be an object".to_string())?; + let toolchain = object + .get("toolchain") + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + .map(str::to_string); + let command = object + .get("command") + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + .map(str::to_string); + let tool = object + .get("tool") + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + .map(str::to_string); + if toolchain.is_none() && command.is_none() { + return Err("Launch executable names neither a toolchain nor a command".to_string()); + } + Ok(Self { + toolchain, + command, + tool, + }) + } +} + +impl PreLaunchStep { + fn from_value(value: &Value) -> Result { + Ok(Self { + executable: LaunchExecutable::from_value( + value + .get("executable") + .ok_or_else(|| "Pre-launch step has no executable".to_string())?, + )?, + arguments: string_array(value.get("arguments")).ok_or_else(|| { + "Pre-launch step arguments must be an array of strings".to_string() + })?, + classpath: string_array(value.get("classpath")).unwrap_or_default(), + modulepath: string_array(value.get("modulepath")).unwrap_or_default(), + }) + } +} + +/// 为 Core 构造 Maven 上下文;上下文只承载项目默认值,不重新推断模块。 +pub fn maven_context_for_configuration(item: &RunConfigItem, toolchains: &ToolchainPaths) -> Value { + let maven = item.extensions.get("maven"); + let profiles = maven + .and_then(|value| value.get("profiles")) + .and_then(Value::as_array) + .map(|items| { + let mut values = items + .iter() + .filter_map(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(str::to_string) + .collect::>(); + values.sort(); + values.dedup(); + values + }) + .unwrap_or_default(); + let reactor_path = maven + .and_then(|value| value.get("reactorPath")) + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + .unwrap_or(item.cwd.as_str()) + .to_string(); + json!({ + "version": 1, + "reactorPath": reactor_path, + "profiles": profiles, + "settingsPath": if toolchains.maven_settings_path.is_empty() { Value::Null } else { json!(toolchains.maven_settings_path) }, + "localRepositoryPath": if toolchains.local_repository_path.is_empty() { Value::Null } else { json!(toolchains.local_repository_path) }, + "skipTests": maven.and_then(|value| value.get("skipTests")).and_then(Value::as_bool).unwrap_or(false), + "mavenExecutablePath": item.maven_executable_path().or(Some(toolchains.maven_executable_path.as_str())).filter(|value| !value.is_empty()).map(Value::from).unwrap_or(Value::Null), + "javaHomePath": item + .maven_java_home_path() + .or(Some(toolchains.maven_java_home_path.as_str())) + .filter(|value| !value.is_empty()) + .map(Value::from) + .unwrap_or(Value::Null), + }) +} + +/// POSIX 下把 Core 的结构化路径合并到已有 JVM 参数,避免第二个 `-cp` 覆盖它。 +pub fn merge_java_paths( + arguments: &[String], + paths: &[String], + default_flag: &str, + existing_flags: &[&str], +) -> Vec { + if paths.is_empty() { + return arguments.to_vec(); + } + let joined = paths.join(":"); + let mut result = arguments.to_vec(); + for index in (0..result.len().saturating_sub(1)).rev() { + if existing_flags.iter().any(|flag| *flag == result[index]) { + result[index + 1] = format!("{joined}:{}", result[index + 1]); + return result; + } + } + let mut with_path = vec![default_flag.to_string(), joined]; + with_path.append(&mut result); + with_path +} + +/// 生成 `runConfig.createLaunchPlan` 请求;Linux 不填充 JDT 专属元数据。 +pub fn create_launch_plan_request( + root: &str, + item: &RunConfigItem, + current_file: Option<&str>, + maven_context: Option<&Value>, + java_launch: Option<&Value>, +) -> Value { + let mut payload = json!({ + "root": root, + "configurationId": item.id, + }); + if let Some(current_file) = current_file { + payload["currentFile"] = json!(current_file); + } + if let Some(context) = maven_context { + payload["mavenContext"] = context.clone(); + } + if let Some(launch) = java_launch { + payload["javaLaunch"] = launch.clone(); + } + payload +} + +fn string_array(value: Option<&Value>) -> Option> { + value? + .as_array()? + .iter() + .map(|item| item.as_str().map(str::to_string)) + .collect() +} + +fn string_map(value: Option<&Value>) -> BTreeMap { + value + .and_then(Value::as_object) + .map(|object| { + object + .iter() + .filter_map(|(key, value)| { + value.as_str().map(|value| (key.clone(), value.to_string())) + }) + .collect() + }) + .unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicU64, Ordering}; + + static TEST_SEQUENCE: AtomicU64 = AtomicU64::new(0); + + struct Fixture(PathBuf); + + impl Drop for Fixture { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } + } + + fn fixture(name: &str) -> Fixture { + let path = std::env::temp_dir().join(format!( + "lithe-linux-config-{name}-{}-{}", + std::process::id(), + TEST_SEQUENCE.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&path).expect("fixture"); + Fixture(path) + } + + fn core_data(command: &str, payload: Value) -> Value { + let request = json!({ + "id": format!("linux-test-{command}"), + "command": command, + "payload": payload + }); + let response: Value = serde_json::from_str(&lithe_core::execute_json(&request.to_string())) + .expect("core response"); + assert_eq!(response["ok"], true, "core response: {response}"); + response["data"].clone() + } + + fn write_generated(root: &Path, configurations: Value) { + let path = root.join(".lithe/run/generated.json"); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write( + path, + serde_json::to_vec(&json!({"version": 2, "configurations": configurations})).unwrap(), + ) + .unwrap(); + } + + #[test] + fn core_plan_keeps_process_and_spring_goals_without_compile_fallback() { + let fixture = fixture("core-plan"); + let root = fixture.0.to_string_lossy().into_owned(); + write_generated( + &fixture.0, + json!([ + { + "id": "process", + "name": "Node process", + "provider": "npm.script", + "command": "node", + "args": ["server.js"], + "toolchains": {"runtime": "project-node"} + }, + { + "id": "spring", + "name": "Spring service", + "provider": "spring-boot.maven", + "toolchains": {"java": "project-jdk", "maven": "project-maven"}, + "extensions": {"maven": {"module": "."}} + } + ]), + ); + let process = core_data( + "runConfig.createLaunchPlan", + json!({"root": root, "configurationId": "process"}), + ); + assert_eq!(process["executable"]["command"], "node"); + assert!(process.get("preLaunchSteps").is_none()); + let spring = core_data( + "runConfig.createLaunchPlan", + json!({"root": root, "configurationId": "spring"}), + ); + let arguments = spring["arguments"].as_array().unwrap(); + assert!(arguments.iter().any(|value| value == "spring-boot:run")); + assert!(!arguments.iter().any(|value| value == "compile")); + assert!(spring.get("preLaunchSteps").is_none()); + } + + #[test] + fn core_plan_uses_javac_then_java_for_standalone_current_file() { + let fixture = fixture("standalone-plan"); + let root = fixture.0.to_string_lossy().into_owned(); + fs::create_dir_all(fixture.0.join("src")).unwrap(); + fs::write( + fixture.0.join("src/App.java"), + "package demo; class App { public static void main(String[] args) {} }", + ) + .unwrap(); + write_generated( + &fixture.0, + json!([{ + "id": "current-file", + "name": "Current File", + "provider": "java.current-file", + "toolchains": {"java": "project-jdk"}, + "extensions": {"java": {}, "maven": {"module": "."}} + }]), + ); + let plan = core_data( + "runConfig.createLaunchPlan", + json!({ + "root": root, + "configurationId": "current-file", + "currentFile": "src/App.java" + }), + ); + assert_eq!(plan["executable"]["toolchain"], "project-jdk"); + assert_eq!(plan["preLaunchSteps"][0]["executable"]["tool"], "javac"); + assert!(plan["arguments"] + .as_array() + .unwrap() + .iter() + .any(|value| value == "demo.App")); + } + + #[test] + fn maven_java_main_without_jdt_metadata_fails_explicitly() { + let fixture = fixture("jdt-not-ready"); + let root = fixture.0.to_string_lossy().into_owned(); + fs::create_dir_all(fixture.0.join("src")).unwrap(); + fs::write(fixture.0.join("src/App.java"), "class App {}").unwrap(); + write_generated( + &fixture.0, + json!([{ + "id": "java-main:demo.App", + "name": "App", + "provider": "java.main", + "toolchains": {"java": "project-jdk", "maven": "project-maven"}, + "extensions": { + "java": {"source": "src/App.java"}, + "maven": {"module": ".", "mainClass": "demo.App"} + } + }]), + ); + let request = json!({ + "id": "linux-test-jdt-not-ready", + "command": "runConfig.createLaunchPlan", + "payload": {"root": root, "configurationId": "java-main:demo.App"} + }); + let response: Value = + serde_json::from_str(&lithe_core::execute_json(&request.to_string())).unwrap(); + assert_eq!(response["ok"], false, "{response}"); + assert!(response["error"]["message"] + .as_str() + .unwrap() + .contains("Java project launch metadata is unavailable")); + } + + #[test] + fn core_toolchain_defaults_are_read_from_canonical_nested_document() { + let fixture = fixture("toolchain"); + let root = fixture.0.to_string_lossy().into_owned(); + write_generated( + &fixture.0, + json!([{ + "id": "current-file", + "name": "Current File", + "provider": "java.current-file", + "toolchains": {"java": "project-jdk"}, + "extensions": {"maven": {"module": "."}} + }]), + ); + let local = fixture.0.join(".lithe/run/local.json"); + fs::create_dir_all(local.parent().unwrap()).unwrap(); + fs::write( + local, + r#"{"version":2,"toolchain":{"java":{"homePath":"/fixture/jdk"},"maven":{"executablePath":"/fixture/mvn","javaHomePath":"/fixture/maven-jdk"}},"configurations":[]}"#, + ) + .unwrap(); + let resolved = core_data( + "runConfig.resolve", + json!({"root": root, "toolchainCandidates": []}), + ); + let config = &resolved["configurations"][0]; + assert_eq!(config["extensions"]["java"]["homePath"], "/fixture/jdk"); + assert_eq!( + config["extensions"]["java"]["mavenJavaHomePath"], + "/fixture/maven-jdk" + ); + } + + #[test] + fn stale_sequence_rejects_previous_reload_and_execution() { + assert!(sequence_is_current(7, 7)); + assert!(!sequence_is_current(6, 7)); + } + + #[test] + fn posix_classpath_merges_into_existing_flag() { + let arguments = vec!["-cp".to_string(), "old".to_string(), "demo.App".to_string()]; + let merged = merge_java_paths( + &arguments, + &["one".to_string(), "two".to_string()], + "-cp", + &["-cp", "-classpath", "--class-path"], + ); + assert_eq!(merged, vec!["-cp", "one:two:old", "demo.App"]); + } + + #[test] + fn toolchain_reader_accepts_legacy_keys_and_writer_replaces_them() { + let fixture = fixture("toolchain-compat"); + let local = fixture.0.join(".lithe/run/local.json"); + fs::create_dir_all(local.parent().unwrap()).unwrap(); + fs::write( + &local, + r#"{"version":2,"toolchain":{"javaHome":"/legacy/jdk","mavenExecutable":"/legacy/mvn","mavenJavaHome":"/legacy/maven-jdk"},"configurations":[]}"#, + ) + .unwrap(); + let paths = read_toolchain_paths(&fixture.0.to_string_lossy()); + assert_eq!(paths.java_home_path, "/legacy/jdk"); + assert_eq!(paths.maven_executable_path, "/legacy/mvn"); + assert_eq!(paths.maven_java_home_path, "/legacy/maven-jdk"); + write_toolchain_paths( + &fixture.0.to_string_lossy(), + &ToolchainPaths { + java_home_path: "/canonical/jdk".to_string(), + maven_executable_path: "/canonical/mvn".to_string(), + maven_java_home_path: "/canonical/maven-jdk".to_string(), + ..ToolchainPaths::default() + }, + ) + .unwrap(); + let value: Value = serde_json::from_str(&fs::read_to_string(local).unwrap()).unwrap(); + assert_eq!(value["toolchain"]["javaHomePath"], "/canonical/jdk"); + assert_eq!(value["toolchain"]["mavenExecutablePath"], "/canonical/mvn"); + assert_eq!( + value["toolchain"]["mavenJavaHomePath"], + "/canonical/maven-jdk" + ); + assert!(value["toolchain"].get("javaHome").is_none()); + assert!(value["toolchain"].get("mavenExecutable").is_none()); + assert!(value["toolchain"].get("mavenJavaHome").is_none()); + } + + #[test] + fn generated_documents_write_all_lithe_sidecars_and_preserve_gitignore() { + let fixture = fixture("documents"); + fs::create_dir_all(fixture.0.join(".lithe")).unwrap(); + fs::write( + fixture.0.join(".lithe/.gitignore"), + "custom/\nrun/local.json\n", + ) + .unwrap(); + write_generated_documents( + &fixture.0.to_string_lossy(), + &json!({"version": 2, "configurations": [{"id": "x", "name": "X", "provider": "npm.script", "command": "node"}]}), + &json!({"version": 1, "toolchains": {}}), + Some("x"), + ) + .unwrap(); + assert!(fixture.0.join(".lithe/run/generated.json").is_file()); + assert!(fixture + .0 + .join(".lithe/toolchains/requirements.json") + .is_file()); + let ignore = fs::read_to_string(fixture.0.join(".lithe/.gitignore")).unwrap(); + assert!(ignore.starts_with("custom/\nrun/local.json\n")); + assert!(ignore.contains("run/classes/\n")); + let manifest: Value = serde_json::from_str( + &fs::read_to_string(fixture.0.join(".lithe/project.json")).unwrap(), + ) + .unwrap(); + assert_eq!(manifest["defaultRunConfiguration"], "x"); + } +} diff --git a/linux/src/workbench/run/mod.rs b/linux/src/workbench/run/mod.rs new file mode 100644 index 000000000..112c48bcf --- /dev/null +++ b/linux/src/workbench/run/mod.rs @@ -0,0 +1,15 @@ +//! Linux Run 与 Maven 执行边界。 +//! +//! Core 负责配置文档、工具链需求和平台无关启动计划;本模块只负责 +//! Linux 宿主侧的文档落盘、工具链解析和进程会话生命周期。 + +pub mod config; +pub mod process; + +pub use config::{ + create_launch_plan_request, default_generated_configuration_id, list_java_sources, + maven_context_for_configuration, parse_resolved_configurations, read_toolchain_paths, + sequence_is_current, toolchain_candidates, write_generated_documents, write_toolchain_paths, + LaunchPlan, RunConfigItem, ToolchainPaths, +}; +pub use process::{OutputStream, ProcessEvent, ProcessManager}; diff --git a/linux/src/workbench/run/process.rs b/linux/src/workbench/run/process.rs new file mode 100644 index 000000000..e78ca8362 --- /dev/null +++ b/linux/src/workbench/run/process.rs @@ -0,0 +1,1331 @@ +//! Linux Run/Maven 进程解析与受管会话。 +//! +//! 每个执行由一个 session/execution 身份拥有。一个后台线程顺序执行所有步骤, +//! stdout/stderr 各自有明确归属并在进程结束后 join;停止操作向独立进程组 +//! 发送 TERM,超时后再 KILL,调用方通过 `ProcessHandle::join` 完成回收。 + +use std::collections::{BTreeMap, HashMap}; +use std::fs; +use std::io::{BufRead, BufReader}; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Stdio}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{mpsc, Arc, Condvar, Mutex}; +use std::thread::{self, JoinHandle}; +use std::time::Duration; + +use serde_json::Value; + +use super::config::{ + merge_java_paths, LaunchExecutable, LaunchPlan, PreLaunchStep, ToolchainPaths, +}; + +const PROCESS_STOP_TIMEOUT: Duration = Duration::from_secs(2); +const PROCESS_POLL_INTERVAL: Duration = Duration::from_millis(20); + +/// 一个已经解析到 Linux 路径和环境的命令。 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProcessCommand { + pub program: String, + pub arguments: Vec, + pub working_directory: String, + pub environment: BTreeMap, +} + +impl ProcessCommand { + fn label(&self) -> String { + format!("{} {}", self.program, self.arguments.join(" ")) + } +} + +/// 一个由同一 session 顺序执行的步骤。 +#[derive(Debug, Clone)] +pub struct ProcessStep { + pub label: String, + pub command: ProcessCommand, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OutputStream { + Stdout, + Stderr, +} + +#[derive(Debug)] +pub enum ProcessEvent { + Started { + index: usize, + label: String, + }, + Output { + stream: OutputStream, + text: String, + }, + Finished { + exit_code: Option, + cancelled: bool, + error: Option, + }, +} + +#[derive(Debug)] +struct Control { + stop_requested: AtomicBool, + pgid: Mutex>, + finished: Mutex, + finished_signal: Condvar, +} + +impl Control { + fn new() -> Self { + Self { + stop_requested: AtomicBool::new(false), + pgid: Mutex::new(None), + finished: Mutex::new(false), + finished_signal: Condvar::new(), + } + } + + fn request_stop(&self) { + self.stop_requested.store(true, Ordering::Release); + } + + fn is_stop_requested(&self) -> bool { + self.stop_requested.load(Ordering::Acquire) + } + + fn set_pgid(&self, pgid: i32) { + if let Ok(mut slot) = self.pgid.lock() { + *slot = Some(pgid); + } + } + + fn pgid(&self) -> Option { + self.pgid.lock().ok().and_then(|value| *value) + } + + fn mark_finished(&self) { + if let Ok(mut pgid) = self.pgid.lock() { + *pgid = None; + } + if let Ok(mut finished) = self.finished.lock() { + *finished = true; + } + self.finished_signal.notify_all(); + } + + fn wait_finished(&self, timeout: Duration) -> bool { + let Ok(finished) = self.finished.lock() else { + return true; + }; + let (finished, timeout_result) = self + .finished_signal + .wait_timeout_while(finished, timeout, |finished| !*finished) + .unwrap(); + *finished || timeout_result.timed_out() + } +} + +struct SessionEntry { + execution_id: String, + control: Arc, + join: Arc>>>, +} + +#[derive(Clone)] +struct ProcessManagerInner { + sessions: Arc>>, +} + +/// 管理所有 Linux Run/Maven session;同一个 session 启动新执行前先停止旧执行。 +#[derive(Clone)] +pub struct ProcessManager { + inner: ProcessManagerInner, +} + +/// 一个可停止、可等待回收的执行句柄。 +pub struct ProcessHandle { + manager: ProcessManager, + session_id: String, + execution_id: String, + control: Arc, + join: Arc>>>, +} + +impl ProcessManager { + pub fn new() -> Self { + Self { + inner: ProcessManagerInner { + sessions: Arc::new(Mutex::new(HashMap::new())), + }, + } + } + + /// 启动一个 session;步骤失败会由同一个 worker 停止后续步骤。 + pub fn start( + &self, + session_id: &str, + execution_id: &str, + steps: Vec, + sender: mpsc::Sender, + ) -> Result { + if session_id.trim().is_empty() || execution_id.trim().is_empty() { + return Err("A process session and execution id are required".to_string()); + } + self.stop(session_id, None).ok(); + + let control = Arc::new(Control::new()); + let join = Arc::new(Mutex::new(None)); + { + let mut sessions = self + .inner + .sessions + .lock() + .map_err(|_| "Process session state is unavailable".to_string())?; + sessions.insert( + session_id.to_string(), + SessionEntry { + execution_id: execution_id.to_string(), + control: control.clone(), + join: join.clone(), + }, + ); + } + + let worker_control = control.clone(); + let worker = thread::Builder::new() + .name("lithe-run-process".to_string()) + .spawn(move || { + let outcome = execute_steps(&steps, &sender, &worker_control); + if let Err(error) = outcome { + let _ = sender.send(ProcessEvent::Finished { + exit_code: Some(1), + cancelled: false, + error: Some(error), + }); + } + worker_control.mark_finished(); + // The entry remains until the owner calls join/reap, so stop can + // still observe the exact execution while its final event drains. + }) + .map_err(|error| { + let mut sessions = self.inner.sessions.lock().ok(); + if let Some(sessions) = sessions.as_mut() { + sessions.remove(session_id); + } + format!("Could not start process worker: {error}") + })?; + if let Ok(mut slot) = join.lock() { + *slot = Some(worker); + } + Ok(ProcessHandle { + manager: self.clone(), + session_id: session_id.to_string(), + execution_id: execution_id.to_string(), + control, + join, + }) + } + + /// 发出停止信号后立即返回;worker 负责有界等待和 join。 + pub fn request_stop(&self, session_id: &str, execution_id: Option<&str>) -> Result<(), String> { + let control = { + let sessions = self + .inner + .sessions + .lock() + .map_err(|_| "Process session state is unavailable".to_string())?; + sessions + .get(session_id) + .filter(|entry| execution_id.is_none_or(|id| entry.execution_id == id)) + .map(|entry| entry.control.clone()) + }; + if let Some(control) = control { + control.request_stop(); + send_signal(control.pgid(), libc::SIGTERM); + } + Ok(()) + } + + /// 停止当前 session;传入 execution id 时只停止匹配的执行。 + pub fn stop(&self, session_id: &str, execution_id: Option<&str>) -> Result<(), String> { + let entry = { + let sessions = self + .inner + .sessions + .lock() + .map_err(|_| "Process session state is unavailable".to_string())?; + sessions + .get(session_id) + .filter(|entry| execution_id.is_none_or(|id| entry.execution_id == id)) + .map(|entry| { + ( + entry.execution_id.clone(), + entry.control.clone(), + entry.join.clone(), + ) + }) + }; + let Some((owned_execution, control, join)) = entry else { + return Ok(()); + }; + stop_control(&control)?; + if let Ok(mut slot) = join.lock() { + if let Some(worker) = slot.take() { + let _ = worker.join(); + } + } + if let Ok(mut sessions) = self.inner.sessions.lock() { + if sessions + .get(session_id) + .is_some_and(|entry| entry.execution_id == owned_execution) + { + sessions.remove(session_id); + } + } + Ok(()) + } + + fn reap(&self, session_id: &str, execution_id: &str) { + let should_remove = { + let Ok(sessions) = self.inner.sessions.lock() else { + return; + }; + sessions + .get(session_id) + .is_some_and(|entry| entry.execution_id == execution_id) + }; + if should_remove { + if let Ok(mut sessions) = self.inner.sessions.lock() { + sessions.remove(session_id); + } + } + } +} + +impl Default for ProcessManager { + fn default() -> Self { + Self::new() + } +} + +impl ProcessHandle { + pub fn stop(&self) -> Result<(), String> { + self.control.request_stop(); + send_signal(self.control.pgid(), libc::SIGTERM); + if !self.control.wait_finished(PROCESS_STOP_TIMEOUT) { + send_signal(self.control.pgid(), libc::SIGKILL); + if !self.control.wait_finished(PROCESS_STOP_TIMEOUT) { + return Err("Process did not exit within the stop deadline".to_string()); + } + } + self.join() + } + + /// 等待 worker 完成并移除 session 条目;必须在消费 Finished 事件后调用。 + pub fn join(&self) -> Result<(), String> { + if !self.control.wait_finished(PROCESS_STOP_TIMEOUT) { + return Err("Process worker did not finish within the join deadline".to_string()); + } + if let Ok(mut slot) = self.join.lock() { + if let Some(worker) = slot.take() { + worker + .join() + .map_err(|_| "Process worker panicked".to_string())?; + } + } + self.manager.reap(&self.session_id, &self.execution_id); + Ok(()) + } +} + +impl Drop for ProcessHandle { + fn drop(&mut self) { + let _ = self.stop(); + } +} + +impl Drop for ProcessManagerInner { + fn drop(&mut self) { + let sessions = self + .sessions + .lock() + .map(|mut sessions| sessions.drain().map(|(_, entry)| entry).collect::>()) + .unwrap_or_default(); + for entry in sessions { + entry.control.request_stop(); + send_signal(entry.control.pgid(), libc::SIGTERM); + let _ = entry.control.wait_finished(PROCESS_STOP_TIMEOUT); + send_signal(entry.control.pgid(), libc::SIGKILL); + if let Ok(mut slot) = entry.join.lock() { + if let Some(worker) = slot.take() { + let _ = worker.join(); + } + } + } + } +} + +fn execute_steps( + steps: &[ProcessStep], + sender: &mpsc::Sender, + control: &Control, +) -> Result<(), String> { + for (index, step) in steps.iter().enumerate() { + if control.is_stop_requested() { + let _ = sender.send(ProcessEvent::Finished { + exit_code: None, + cancelled: true, + error: None, + }); + return Ok(()); + } + if sender + .send(ProcessEvent::Started { + index, + label: step.label.clone(), + }) + .is_err() + { + return Ok(()); + } + let outcome = execute_step(&step.command, sender, control)?; + let Some(code) = outcome else { + let _ = sender.send(ProcessEvent::Finished { + exit_code: None, + cancelled: true, + error: None, + }); + return Ok(()); + }; + if code != 0 { + let _ = sender.send(ProcessEvent::Finished { + exit_code: Some(code), + cancelled: false, + error: None, + }); + return Ok(()); + } + } + let _ = sender.send(ProcessEvent::Finished { + exit_code: Some(0), + cancelled: false, + error: None, + }); + Ok(()) +} + +/// Returns `Some(exit_code)` for a completed child, or `None` when stopped. +fn execute_step( + spec: &ProcessCommand, + sender: &mpsc::Sender, + control: &Control, +) -> Result, String> { + let mut command = Command::new(&spec.program); + command + .args(&spec.arguments) + .current_dir(&spec.working_directory) + .envs(&spec.environment) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + configure_process_group(&mut command); + let mut child = command + .spawn() + .map_err(|error| format!("Unable to start process {}: {error}", spec.program))?; + let pgid = child.id() as i32; + control.set_pgid(pgid); + let stdout = child.stdout.take(); + let stderr = child.stderr.take(); + let stdout_thread = spawn_reader(stdout, OutputStream::Stdout, sender.clone()); + let stderr_thread = spawn_reader(stderr, OutputStream::Stderr, sender.clone()); + + let status = wait_for_child(&mut child, control); + // A child can exit while a forked descendant still owns the pipe. Kill the + // whole process group before joining readers so the reader threads cannot + // remain blocked on inherited descriptors. + send_signal(Some(pgid), libc::SIGKILL); + if let Some(thread) = stdout_thread { + let _ = thread.join(); + } + if let Some(thread) = stderr_thread { + let _ = thread.join(); + } + if control.is_stop_requested() { + return Ok(None); + } + let status = status?; + status.code().map(Some).ok_or_else(|| { + format!( + "Process {} was terminated without an exit code", + spec.program + ) + }) +} + +fn spawn_reader( + reader: Option, + stream: OutputStream, + sender: mpsc::Sender, +) -> Option> +where + R: std::io::Read + Send + 'static, +{ + let reader = reader?; + Some(thread::spawn(move || { + for line in BufReader::new(reader).lines() { + let Ok(line) = line else { break }; + if sender + .send(ProcessEvent::Output { stream, text: line }) + .is_err() + { + break; + } + } + })) +} + +fn wait_for_child( + child: &mut Child, + control: &Control, +) -> Result { + loop { + match child.try_wait() { + Ok(Some(status)) => return Ok(status), + Ok(None) if control.is_stop_requested() => { + send_signal(control.pgid(), libc::SIGTERM); + thread::park_timeout(PROCESS_STOP_TIMEOUT.min(Duration::from_millis(200))); + send_signal(control.pgid(), libc::SIGKILL); + return child + .wait() + .map_err(|error| format!("Waiting for process failed: {error}")); + } + Ok(None) => thread::park_timeout(PROCESS_POLL_INTERVAL), + Err(error) => return Err(format!("Waiting for process failed: {error}")), + } + } +} + +fn stop_control(control: &Control) -> Result<(), String> { + control.request_stop(); + send_signal(control.pgid(), libc::SIGTERM); + if !control.wait_finished(PROCESS_STOP_TIMEOUT) { + send_signal(control.pgid(), libc::SIGKILL); + if !control.wait_finished(PROCESS_STOP_TIMEOUT) { + return Err("Process did not exit within the stop deadline".to_string()); + } + } + Ok(()) +} + +fn configure_process_group(command: &mut Command) { + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + command.process_group(0); + } + #[cfg(not(unix))] + { + let _ = command; + } +} + +fn send_signal(pgid: Option, signal: i32) { + if let Some(pgid) = pgid.filter(|value| *value > 1) { + unsafe { + libc::kill(-pgid, signal); + } + } +} + +/// 解析 Core 计划中的主进程和每个 pre-launch 步骤。 +pub fn resolve_launch( + plan: &LaunchPlan, + root: &str, + toolchains: &ToolchainPaths, +) -> Result<(Vec, ProcessCommand), String> { + let working_directory = resolve_working_directory(root, &plan.working_directory)?; + let mut environment = base_environment(); + let java_home = configured_java_home(root, toolchains)?; + let maven_java_home = configured_maven_java_home(root, toolchains, java_home.as_deref())?; + apply_plan_environment( + &mut environment, + &plan.environment, + java_home.as_deref(), + maven_java_home.as_deref(), + ); + environment.extend(plan.env.clone()); + if plan.executable.toolchain.as_deref() == Some("project-maven") + && !plan.env.contains_key("JAVA_HOME") + { + if let Some(home) = maven_java_home.as_deref() { + environment.insert("JAVA_HOME".to_string(), home.to_string()); + } + } + let main_executable = resolve_executable( + root, + &working_directory, + &plan.executable, + toolchains, + java_home.as_deref(), + )?; + let mut main_arguments = merge_java_paths( + &plan.arguments, + &resolve_paths(root, &plan.classpath)?, + "-cp", + &["-cp", "-classpath", "--class-path"], + ); + main_arguments = merge_java_paths( + &main_arguments, + &resolve_paths(root, &plan.modulepath)?, + "--module-path", + &["-p", "--module-path"], + ); + prepend_executable_path(&mut environment, &main_executable); + let main = ProcessCommand { + program: main_executable, + arguments: main_arguments, + working_directory: working_directory.clone(), + environment: environment.clone(), + }; + + let mut steps = Vec::with_capacity(plan.pre_launch_steps.len() + 1); + for step in &plan.pre_launch_steps { + steps.push(resolve_prelaunch_step( + root, + &working_directory, + step, + toolchains, + java_home.as_deref(), + &environment, + )?); + } + steps.push(ProcessStep { + label: format!("$ {}", main.label()), + command: main.clone(), + }); + Ok((steps, main)) +} + +fn resolve_prelaunch_step( + root: &str, + working_directory: &str, + step: &PreLaunchStep, + toolchains: &ToolchainPaths, + java_home: Option<&str>, + base_environment: &BTreeMap, +) -> Result { + let executable = resolve_executable( + root, + working_directory, + &step.executable, + toolchains, + java_home, + )?; + prepare_prelaunch_output_directory(root, &step.executable, &step.arguments)?; + let mut arguments = merge_java_paths( + &step.arguments, + &resolve_paths(root, &step.classpath)?, + "-cp", + &["-cp", "-classpath", "--class-path"], + ); + arguments = merge_java_paths( + &arguments, + &resolve_paths(root, &step.modulepath)?, + "--module-path", + &["-p", "--module-path"], + ); + let mut environment = base_environment.clone(); + prepend_executable_path(&mut environment, &executable); + let command = ProcessCommand { + program: executable.clone(), + arguments, + working_directory: working_directory.to_string(), + environment, + }; + Ok(ProcessStep { + label: format!("$ {} {}", executable, command.arguments.join(" ")), + command, + }) +} + +fn prepare_prelaunch_output_directory( + root: &str, + executable: &LaunchExecutable, + arguments: &[String], +) -> Result<(), String> { + let is_javac = executable.tool.as_deref() == Some("javac") + || executable.command.as_deref().is_some_and(|command| { + Path::new(command) + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name == "javac") + }); + if !is_javac { + return Ok(()); + } + for (index, argument) in arguments.iter().enumerate() { + let output = if argument == "-d" || argument == "--output-dir" { + arguments.get(index + 1).ok_or_else(|| { + format!("Compiler output option {argument} is missing its directory") + })? + } else if let Some(value) = argument.strip_prefix("--output-dir=") { + value + } else { + continue; + }; + let path = Path::new(output); + if !path.is_absolute() + && path + .components() + .any(|component| matches!(component, std::path::Component::ParentDir)) + { + return Err("Compiler output directory must stay inside the project.".to_string()); + } + let output_path = if path.is_absolute() { + path.to_path_buf() + } else { + Path::new(root).join(path) + }; + fs::create_dir_all(&output_path) + .map_err(|error| format!("Unable to create compiler output directory: {error}"))?; + } + Ok(()) +} + +fn configured_java_home(root: &str, toolchains: &ToolchainPaths) -> Result, String> { + if let Some(path) = non_empty(&toolchains.java_home_path) { + let selected = absolute_path(root, &path); + return selected_java_home(&selected) + .map(|home| Some(home.to_string_lossy().into_owned())) + .ok_or_else(|| { + format!( + "The selected JDK does not contain bin/java: {}", + selected.to_string_lossy() + ) + }); + } + Ok(std::env::var_os("JAVA_HOME") + .map(PathBuf::from) + .map(|path| path.to_string_lossy().into_owned()) + .filter(|path| jdk_tool(path, "java").is_some()) + .or_else(|| { + lookup_on_path("java") + .and_then(|path| path.parent().and_then(Path::parent).map(Path::to_path_buf)) + .map(|path| path.to_string_lossy().into_owned()) + .filter(|path| jdk_tool(path, "java").is_some()) + })) +} + +fn configured_maven_java_home( + root: &str, + toolchains: &ToolchainPaths, + java_home: Option<&str>, +) -> Result, String> { + if let Some(path) = non_empty(&toolchains.maven_java_home_path) { + let selected = absolute_path(root, &path); + return selected_java_home(&selected) + .map(|home| Some(home.to_string_lossy().into_owned())) + .ok_or_else(|| { + format!( + "The selected Maven JDK does not contain bin/java: {}", + selected.to_string_lossy() + ) + }); + } + Ok(java_home.map(str::to_string)) +} + +fn selected_java_home(path: &Path) -> Option { + if jdk_tool(&path.to_string_lossy(), "java").is_some() { + return Some(path.to_path_buf()); + } + if path.is_file() + && path + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name == "java") + { + let home = path.parent().and_then(Path::parent)?; + if jdk_tool(&home.to_string_lossy(), "java").is_some() { + return Some(home.to_path_buf()); + } + } + None +} + +fn non_empty(value: &str) -> Option { + let value = value.trim(); + (!value.is_empty()).then(|| value.to_string()) +} + +fn resolve_executable( + root: &str, + working_directory: &str, + executable: &LaunchExecutable, + toolchains: &ToolchainPaths, + java_home: Option<&str>, +) -> Result { + if let Some(toolchain) = executable.toolchain.as_deref() { + return match toolchain { + "project-jdk" => { + let home = java_home.ok_or_else(|| { + "No Java runtime was found. Set javaHomePath or install a JDK.".to_string() + })?; + let tool = executable.tool.as_deref().unwrap_or("java"); + jdk_tool(&home, tool) + .ok_or_else(|| format!("The selected JDK does not contain bin/{tool}.")) + } + "project-maven" => { + resolve_maven(root, working_directory, &toolchains.maven_executable_path) + } + "project-node" => { + resolve_runtime(root, "project-node", &toolchains.runtime_executable_paths) + } + other => resolve_runtime(root, other, &toolchains.runtime_executable_paths), + }; + } + let command = executable + .command + .as_deref() + .ok_or_else(|| "Launch plan names neither a toolchain nor a command".to_string())?; + resolve_command( + root, + working_directory, + command, + &toolchains.runtime_executable_paths, + ) +} + +fn resolve_runtime( + root: &str, + id: &str, + paths: &BTreeMap, +) -> Result { + if let Some(configured) = paths.get(id).and_then(|value| non_empty(value)) { + let path = absolute_path(root, &configured); + if path.is_file() { + return Ok(path.to_string_lossy().into_owned()); + } + if id == "project-node" { + for name in ["node", "node.exe", "npm", "npm.cmd"] { + let direct = path.join(name); + if direct.is_file() { + return Ok(direct.to_string_lossy().into_owned()); + } + let nested = path.join("bin").join(name); + if nested.is_file() { + return Ok(nested.to_string_lossy().into_owned()); + } + } + } + return Err(format!("Configured executable for {id} does not exist.")); + } + let names = if id == "project-node" { + vec!["node", "node.exe"] + } else { + vec![id] + }; + names + .iter() + .find_map(|name| lookup_on_path(name)) + .map(|path| path.to_string_lossy().into_owned()) + .ok_or_else(|| format!("No executable was found for toolchain {id}.")) +} + +fn resolve_command( + root: &str, + working_directory: &str, + command: &str, + runtime_paths: &BTreeMap, +) -> Result { + let command_path = Path::new(command); + if command_path.is_absolute() { + if command_path.is_file() { + return Ok(command_path.to_string_lossy().into_owned()); + } + return Err(format!("Could not find executable: {command}")); + } + if command_path.components().count() > 1 { + for base in [working_directory, root] { + let candidate = Path::new(base).join(command_path); + if candidate.is_file() { + return Ok(candidate.to_string_lossy().into_owned()); + } + } + } + let lower = command.to_ascii_lowercase(); + if matches!( + lower.as_str(), + "node" | "node.exe" | "npm" | "npm.cmd" | "pnpm" | "pnpm.cmd" | "yarn" | "yarn.cmd" + ) { + if let Some(selected) = runtime_paths + .get("project-node") + .and_then(|value| non_empty(value)) + { + let directory = absolute_path(root, &selected); + let directory = if directory.is_dir() { + directory + } else { + directory + .parent() + .unwrap_or_else(|| Path::new(root)) + .to_path_buf() + }; + for name in command_file_names(command) { + let direct = directory.join(&name); + if direct.is_file() { + return Ok(direct.to_string_lossy().into_owned()); + } + let nested = directory.join("bin").join(name); + if nested.is_file() { + return Ok(nested.to_string_lossy().into_owned()); + } + } + return Err(format!("Selected Node runtime does not contain {command}.")); + } + } + command_file_names(command) + .iter() + .find_map(|name| lookup_on_path(name)) + .map(|path| path.to_string_lossy().into_owned()) + .ok_or_else(|| format!("Could not find executable: {command}")) +} + +fn resolve_maven(root: &str, working_directory: &str, configured: &str) -> Result { + if let Some(configured) = non_empty(configured) { + if !configured.contains('/') && !configured.contains('\\') { + if let Some(path) = lookup_on_path(&configured) { + return Ok(path.to_string_lossy().into_owned()); + } + } + let path = absolute_path(root, &configured); + for candidate in [path.clone(), path.join("bin/mvn"), path.join("mvn")] { + if candidate.is_file() { + return Ok(candidate.to_string_lossy().into_owned()); + } + } + return Err("Maven executable path does not exist.".to_string()); + } + for directory in [working_directory, root] { + let wrapper = Path::new(directory).join("mvnw"); + if wrapper.is_file() + && Path::new(directory) + .join(".mvn/wrapper/maven-wrapper.properties") + .is_file() + { + return Ok(wrapper.to_string_lossy().into_owned()); + } + } + lookup_on_path("mvn") + .map(|path| path.to_string_lossy().into_owned()) + .ok_or_else(|| { + "No Maven executable was found. Edit this service configuration.".to_string() + }) +} + +fn command_file_names(command: &str) -> Vec { + if Path::new(command).extension().is_some() { + return vec![command.to_string()]; + } + vec![ + format!("{command}.exe"), + format!("{command}.cmd"), + format!("{command}.bat"), + command.to_string(), + ] +} + +fn lookup_on_path(name: &str) -> Option { + let path = std::env::var_os("PATH")?; + std::env::split_paths(&path) + .map(|directory| directory.join(name)) + .find(|candidate| candidate.is_file()) +} + +fn jdk_tool(home: &str, tool: &str) -> Option { + let path = Path::new(home).join("bin").join(tool); + path.is_file().then(|| path.to_string_lossy().into_owned()) +} + +fn resolve_working_directory(root: &str, relative: &str) -> Result { + let relative = relative.trim(); + if relative.is_empty() || relative == "." { + return Ok(root.to_string()); + } + let candidate = Path::new(relative); + if candidate.is_absolute() + || candidate + .components() + .any(|component| matches!(component, std::path::Component::ParentDir)) + { + return Err("Working directory must stay inside the project.".to_string()); + } + let path = Path::new(root).join(candidate); + if !path.is_dir() { + return Err(format!("Working directory does not exist: {relative}")); + } + Ok(path.to_string_lossy().into_owned()) +} + +fn resolve_paths(root: &str, values: &[String]) -> Result, String> { + values + .iter() + .map(|value| { + let path = Path::new(value); + if path.is_absolute() { + Ok(value.clone()) + } else { + if path + .components() + .any(|component| matches!(component, std::path::Component::ParentDir)) + { + return Err("Java path entries must stay inside the project.".to_string()); + } + Ok(Path::new(root).join(path).to_string_lossy().into_owned()) + } + }) + .collect() +} + +fn apply_plan_environment( + environment: &mut BTreeMap, + values: &BTreeMap, + java_home: Option<&str>, + maven_java_home: Option<&str>, +) { + for (key, value) in values { + let resolved = match value { + Value::String(value) => Some(value.clone()), + Value::Object(object) => { + let toolchain = object.get("toolchain").and_then(Value::as_str); + let property = object + .get("property") + .and_then(Value::as_str) + .unwrap_or("home"); + if property != "home" { + None + } else { + match toolchain { + Some("project-maven") => maven_java_home.map(str::to_string), + _ => java_home.map(str::to_string), + } + } + } + _ => None, + }; + if let Some(value) = resolved { + environment.insert(key.clone(), value); + } + } + if let Some(home) = java_home { + environment.insert("JAVA_HOME".to_string(), home.to_string()); + } +} + +fn base_environment() -> BTreeMap { + std::env::vars().collect() +} + +fn prepend_executable_path(environment: &mut BTreeMap, executable: &str) { + let Some(parent) = Path::new(executable).parent() else { + return; + }; + let mut directories = vec![parent.to_path_buf()]; + if let Some(path) = environment + .get("PATH") + .map(String::as_str) + .or_else(|| environment.get("Path").map(String::as_str)) + { + directories.extend(std::env::split_paths(path)); + } + directories.dedup(); + if let Ok(path) = std::env::join_paths(directories) { + environment.insert("PATH".to_string(), path.to_string_lossy().into_owned()); + } +} + +fn absolute_path(root: &str, value: &str) -> PathBuf { + let path = Path::new(value); + if path.is_absolute() { + path.to_path_buf() + } else { + Path::new(root).join(path) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::sync::atomic::{AtomicU64, Ordering}; + use std::time::Instant; + + static TEST_SEQUENCE: AtomicU64 = AtomicU64::new(0); + + struct Fixture { + root: PathBuf, + manager: ProcessManager, + } + + impl Drop for Fixture { + fn drop(&mut self) { + let _ = self.manager.stop("test", None); + let _ = fs::remove_dir_all(&self.root); + } + } + + fn fixture(name: &str) -> Fixture { + let root = std::env::temp_dir().join(format!( + "lithe-linux-process-{name}-{}-{}", + std::process::id(), + TEST_SEQUENCE.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&root).expect("fixture"); + Fixture { + root, + manager: ProcessManager::new(), + } + } + + fn shell_step(root: &Path, script: &str, label: &str) -> ProcessStep { + ProcessStep { + label: label.to_string(), + command: ProcessCommand { + program: "/bin/sh".to_string(), + arguments: vec!["-c".to_string(), script.to_string()], + working_directory: root.to_string_lossy().into_owned(), + environment: std::env::vars().collect(), + }, + } + } + + fn wait_for_finished( + receiver: &mpsc::Receiver, + deadline: Duration, + ) -> Vec { + let start = Instant::now(); + let mut events = Vec::new(); + while start.elapsed() < deadline { + let remaining = deadline.saturating_sub(start.elapsed()); + match receiver.recv_timeout(remaining) { + Ok(event) => { + let finished = matches!(&event, ProcessEvent::Finished { .. }); + events.push(event); + if finished { + return events; + } + } + Err(mpsc::RecvTimeoutError::Timeout) => break, + Err(mpsc::RecvTimeoutError::Disconnected) => break, + } + } + events + } + + #[test] + fn prelaunch_failure_stops_before_main_step() { + let fixture = fixture("prelaunch-failure"); + let marker = fixture.root.join("main-ran"); + let marker_text = marker.to_string_lossy().into_owned(); + let steps = vec![ + shell_step(&fixture.root, "exit 7", "compiler"), + shell_step( + &fixture.root, + &format!("printf done > '{marker_text}'"), + "main", + ), + ]; + let (sender, receiver) = mpsc::channel(); + let handle = fixture + .manager + .start("test", "execution-1", steps, sender) + .expect("start"); + let events = wait_for_finished(&receiver, Duration::from_secs(2)); + handle.join().expect("join"); + assert!(events.iter().any(|event| matches!( + event, + ProcessEvent::Finished { + exit_code: Some(7), + .. + } + ))); + assert!( + !marker.exists(), + "main step must not run after prelaunch failure" + ); + } + + #[test] + fn stop_terminates_the_complete_process_group() { + let fixture = fixture("process-group"); + let script = "sleep 30 & child=$!; printf '%s\\n' \"$child\"; wait"; + let steps = vec![shell_step(&fixture.root, script, "tree")]; + let (sender, receiver) = mpsc::channel(); + let handle = fixture + .manager + .start("test", "execution-2", steps, sender) + .expect("start"); + let mut child_pid = None; + let deadline = Instant::now() + Duration::from_secs(2); + while Instant::now() < deadline && child_pid.is_none() { + let remaining = deadline.saturating_duration_since(Instant::now()); + match receiver.recv_timeout(remaining) { + Ok(ProcessEvent::Output { text, .. }) => { + if let Ok(pid) = text.trim().parse::() { + child_pid = Some(pid); + } + } + Ok(ProcessEvent::Finished { .. }) => break, + Ok(_) => {} + Err(mpsc::RecvTimeoutError::Timeout) + | Err(mpsc::RecvTimeoutError::Disconnected) => break, + } + } + let child_pid = child_pid.expect("fake process must report its child pid"); + fixture + .manager + .request_stop("test", Some("execution-2")) + .unwrap(); + let events = wait_for_finished(&receiver, Duration::from_secs(2)); + handle.join().expect("join"); + assert!(events.iter().any(|event| matches!( + event, + ProcessEvent::Finished { + cancelled: true, + .. + } + ))); + let gone_deadline = Instant::now() + Duration::from_secs(2); + while Instant::now() < gone_deadline { + let result = unsafe { libc::kill(child_pid, 0) }; + if result == -1 { + break; + } + thread::park_timeout(Duration::from_millis(10)); + } + assert_eq!( + unsafe { libc::kill(child_pid, 0) }, + -1, + "child process survived group stop" + ); + } + + #[test] + fn executable_toolchain_and_tool_map_to_linux_paths() { + let fixture = fixture("toolchain-map"); + let jdk = fixture.root.join("jdk"); + fs::create_dir_all(jdk.join("bin")).unwrap(); + fs::write(jdk.join("bin/java"), "java fixture").unwrap(); + fs::write(jdk.join("bin/javac"), "javac fixture").unwrap(); + let maven = fixture.root.join("maven/bin"); + fs::create_dir_all(&maven).unwrap(); + fs::write(maven.join("mvn"), "maven fixture").unwrap(); + let node = fixture.root.join("node/bin"); + fs::create_dir_all(&node).unwrap(); + fs::write(node.join("node"), "node fixture").unwrap(); + fs::write(node.join("npm"), "npm fixture").unwrap(); + let paths = ToolchainPaths { + java_home_path: jdk.to_string_lossy().into_owned(), + maven_executable_path: fixture.root.join("maven").to_string_lossy().into_owned(), + runtime_executable_paths: BTreeMap::from([( + "project-node".to_string(), + fixture.root.join("node/bin").to_string_lossy().into_owned(), + )]), + ..ToolchainPaths::default() + }; + let plan = LaunchPlan { + executable: LaunchExecutable { + toolchain: Some("project-jdk".to_string()), + command: None, + tool: Some("javac".to_string()), + }, + arguments: vec!["-version".to_string()], + working_directory: ".".to_string(), + environment: BTreeMap::new(), + env: BTreeMap::new(), + pre_launch_steps: vec![PreLaunchStep { + executable: LaunchExecutable { + toolchain: Some("project-jdk".to_string()), + command: None, + tool: Some("javac".to_string()), + }, + arguments: vec!["-version".to_string()], + classpath: Vec::new(), + modulepath: Vec::new(), + }], + classpath: Vec::new(), + modulepath: Vec::new(), + }; + let (steps, main) = resolve_launch(&plan, &fixture.root.to_string_lossy(), &paths).unwrap(); + assert!(main.program.ends_with("jdk/bin/javac")); + assert_eq!(steps.len(), 2); + assert!(steps[0].command.program.ends_with("jdk/bin/javac")); + + let mut maven_plan = plan.clone(); + maven_plan.executable = LaunchExecutable { + toolchain: Some("project-maven".to_string()), + command: None, + tool: None, + }; + maven_plan.pre_launch_steps.clear(); + let (_, maven) = + resolve_launch(&maven_plan, &fixture.root.to_string_lossy(), &paths).unwrap(); + assert!(maven.program.ends_with("maven/bin/mvn")); + + let mut node_plan = plan.clone(); + node_plan.executable = LaunchExecutable { + toolchain: None, + command: Some("npm".to_string()), + tool: None, + }; + node_plan.pre_launch_steps.clear(); + let (_, npm) = resolve_launch(&node_plan, &fixture.root.to_string_lossy(), &paths).unwrap(); + assert!(npm.program.ends_with("node/bin/npm")); + } + + #[test] + fn javac_prelaunch_creates_its_output_directory() { + let fixture = fixture("javac-output"); + let jdk = fixture.root.join("jdk"); + fs::create_dir_all(jdk.join("bin")).unwrap(); + fs::write(jdk.join("bin/java"), "java fixture").unwrap(); + fs::write(jdk.join("bin/javac"), "javac fixture").unwrap(); + let paths = ToolchainPaths { + java_home_path: jdk.to_string_lossy().into_owned(), + ..ToolchainPaths::default() + }; + let plan = LaunchPlan { + executable: LaunchExecutable { + toolchain: Some("project-jdk".to_string()), + command: None, + tool: None, + }, + arguments: vec!["demo.App".to_string()], + working_directory: ".".to_string(), + environment: BTreeMap::new(), + env: BTreeMap::new(), + pre_launch_steps: vec![PreLaunchStep { + executable: LaunchExecutable { + toolchain: Some("project-jdk".to_string()), + command: None, + tool: Some("javac".to_string()), + }, + arguments: vec![ + "-d".to_string(), + ".lithe/run/classes/current-file".to_string(), + "src/App.java".to_string(), + ], + classpath: Vec::new(), + modulepath: Vec::new(), + }], + classpath: vec![".lithe/run/classes/current-file".to_string()], + modulepath: Vec::new(), + }; + resolve_launch(&plan, &fixture.root.to_string_lossy(), &paths).unwrap(); + assert!(fixture + .root + .join(".lithe/run/classes/current-file") + .is_dir()); + } +} diff --git a/linux/src/workbench/search_everywhere.rs b/linux/src/workbench/search_everywhere.rs new file mode 100644 index 000000000..e34eddab0 --- /dev/null +++ b/linux/src/workbench/search_everywhere.rs @@ -0,0 +1,629 @@ +use gpui_kit::assets::IconName; +use gpui_kit::component::button::{Button, ButtonVariants as _}; +use gpui_kit::component::scroll::ScrollableElement as _; +use gpui_kit::component::{h_flex, v_flex, Icon, Sizable as _}; +use gpui_kit::prelude::FluentBuilder as _; +use gpui_kit::{ + div, px, rgba, Context, EventEmitter, FocusHandle, FontWeight, InteractiveElement as _, + IntoElement, KeyDownEvent, ParentElement as _, Render, StatefulInteractiveElement as _, + Styled as _, Window, +}; + +use crate::theme::ThemeColors; + +/// Search Everywhere 动作本地化显示名(id → 菜单键),未知 id 回退 id 本身。 +fn action_display_name(id: &str, cx: &gpui_kit::App) -> String { + let key = match id { + "workbench.new_file" => Some("menu.newFile"), + "workbench.save" => Some("menu.save"), + "workbench.close_tab" => Some("menu.closeTab"), + "workbench.toggle_terminal" => Some("menu.toggleTerminal"), + "workbench.toggle_sidebar" => Some("menu.toggleSecondarySidebar"), + "workbench.open_settings" => Some("menu.preferences"), + "workbench.refresh_workspace" => Some("ui.refresh"), + "workbench.run" => Some("menu.run"), + "workbench.debug" => Some("menu.startDebugging"), + _ => None, + }; + match key { + Some(key) => crate::i18n::menu_text(cx, key).to_string(), + None if id == "workbench.clear_terminal" => format!( + "{}{}", + crate::i18n::menu_text(cx, "ui.clear"), + crate::i18n::menu_text(cx, "menu.terminal") + ), + None => id.to_string(), + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SearchScope { + All, + Files, + Actions, +} + +#[derive(Debug, Clone)] +pub enum SearchEverywhereEvent { + OpenFile(String), + ExecuteAction(String), + Close, +} + +#[derive(Debug, Clone)] +pub struct SearchActionItem { + pub id: String, + pub name: String, + pub shortcut: Option, + pub icon: IconName, +} + +#[derive(Debug, Clone)] +pub enum MatchedItem { + File { + path: String, + name: String, + dir: String, + }, + Action { + id: String, + shortcut: Option, + icon: IconName, + }, +} + +/// IntelliJ / macOS Lithe 风格居中全局搜索模态弹窗 +pub struct SearchEverywhereModal { + pub query: String, + pub scope: SearchScope, + pub files: Vec, + pub actions: Vec, + pub selected_index: usize, + pub focus_handle: FocusHandle, +} + +impl EventEmitter for SearchEverywhereModal {} + +impl SearchEverywhereModal { + pub fn new(cx: &mut Context) -> Self { + let actions = vec![ + SearchActionItem { + id: "workbench.new_file".to_string(), + name: "New File".to_string(), + shortcut: Some("Ctrl+N".to_string()), + icon: IconName::FilePlus, + }, + SearchActionItem { + id: "workbench.save".to_string(), + name: "Save Active File".to_string(), + shortcut: Some("Ctrl+S".to_string()), + icon: IconName::Save, + }, + SearchActionItem { + id: "workbench.close_tab".to_string(), + name: "Close Active Tab".to_string(), + shortcut: Some("Ctrl+W".to_string()), + icon: IconName::Close, + }, + SearchActionItem { + id: "workbench.toggle_terminal".to_string(), + name: "Toggle Terminal".to_string(), + shortcut: Some("Ctrl+`".to_string()), + icon: IconName::Terminal, + }, + SearchActionItem { + id: "workbench.toggle_sidebar".to_string(), + name: "Toggle Sidebar".to_string(), + shortcut: Some("Ctrl+B".to_string()), + icon: IconName::PanelLeft, + }, + SearchActionItem { + id: "workbench.open_settings".to_string(), + name: "Open Settings".to_string(), + shortcut: Some("Ctrl+,".to_string()), + icon: IconName::Settings, + }, + SearchActionItem { + id: "workbench.refresh_workspace".to_string(), + name: "Refresh Workspace".to_string(), + shortcut: Some("Ctrl+R".to_string()), + icon: IconName::RotateCw, + }, + SearchActionItem { + id: "workbench.run".to_string(), + name: "Run Project".to_string(), + shortcut: Some("Ctrl+F5".to_string()), + icon: IconName::Play, + }, + SearchActionItem { + id: "workbench.debug".to_string(), + name: "Debug Project".to_string(), + shortcut: Some("F5".to_string()), + icon: IconName::Bug, + }, + SearchActionItem { + id: "workbench.clear_terminal".to_string(), + name: "Clear Terminal".to_string(), + shortcut: Some("Ctrl+K".to_string()), + icon: IconName::Trash, + }, + ]; + + Self { + query: String::new(), + scope: SearchScope::All, + files: Vec::new(), + actions, + selected_index: 0, + focus_handle: cx.focus_handle(), + } + } + + pub fn set_files(&mut self, files: Vec, cx: &mut Context) { + self.files = files; + cx.notify(); + } + + pub fn reset(&mut self, cx: &mut Context) { + self.query.clear(); + self.selected_index = 0; + cx.notify(); + } + + fn filtered_items(&self, cx: &gpui_kit::App) -> Vec { + let q = self.query.trim().to_lowercase(); + let mut results = Vec::new(); + + // 1. Files + if self.scope == SearchScope::All || self.scope == SearchScope::Files { + for file in &self.files { + if q.is_empty() || file.to_lowercase().contains(&q) { + let (dir, name) = if let Some((d, n)) = file.rsplit_once('/') { + (d.to_string(), n.to_string()) + } else { + (String::new(), file.clone()) + }; + results.push(MatchedItem::File { + path: file.clone(), + name, + dir, + }); + } + } + } + + // 2. Actions(本地化显示名、英文原名、id 任一包含即命中) + if self.scope == SearchScope::All || self.scope == SearchScope::Actions { + for act in &self.actions { + let display = action_display_name(&act.id, cx); + if q.is_empty() + || display.to_lowercase().contains(&q) + || act.name.to_lowercase().contains(&q) + || act.id.to_lowercase().contains(&q) + { + results.push(MatchedItem::Action { + id: act.id.clone(), + shortcut: act.shortcut.clone(), + icon: act.icon, + }); + } + } + } + + results + } + + fn select_item(&self, item: &MatchedItem, cx: &mut Context) { + match item { + MatchedItem::File { path, .. } => { + cx.emit(SearchEverywhereEvent::OpenFile(path.clone())); + } + MatchedItem::Action { id, .. } => { + cx.emit(SearchEverywhereEvent::ExecuteAction(id.clone())); + } + } + } +} + +fn is_code_file(name: &str) -> bool { + let lower = name.to_lowercase(); + lower.ends_with(".rs") + || lower.ends_with(".js") + || lower.ends_with(".ts") + || lower.ends_with(".jsx") + || lower.ends_with(".tsx") + || lower.ends_with(".json") + || lower.ends_with(".toml") + || lower.ends_with(".html") + || lower.ends_with(".css") + || lower.ends_with(".java") + || lower.ends_with(".c") + || lower.ends_with(".cpp") + || lower.ends_with(".h") + || lower.ends_with(".py") + || lower.ends_with(".go") + || lower.ends_with(".swift") + || lower.ends_with(".sh") + || lower.ends_with(".sql") +} + +impl Render for SearchEverywhereModal { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + // 请求聚焦以接收按键输入 + window.focus(&self.focus_handle, cx); + + let filtered = self.filtered_items(cx); + let current_index = if filtered.is_empty() { + 0 + } else { + self.selected_index.min(filtered.len() - 1) + }; + + // 全屏半透明遮罩背景 + div() + .id("search-everywhere-backdrop") + .track_focus(&self.focus_handle) + .absolute() + .inset_0() + .bg(rgba(0x00000088)) + .flex() + .items_center() + .justify_center() + .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { + let key = event.keystroke.key.as_str(); + match key { + "escape" => { + cx.emit(SearchEverywhereEvent::Close); + } + "up" | "arrowup" => { + if this.selected_index > 0 { + this.selected_index -= 1; + cx.notify(); + } + } + "down" | "arrowdown" => { + let total = this.filtered_items(cx).len(); + if total > 0 && this.selected_index + 1 < total { + this.selected_index += 1; + cx.notify(); + } + } + "enter" => { + let items = this.filtered_items(cx); + let idx = if items.is_empty() { + 0 + } else { + this.selected_index.min(items.len() - 1) + }; + if let Some(item) = items.get(idx) { + this.select_item(item, cx); + } + } + "backspace" => { + this.query.pop(); + this.selected_index = 0; + cx.notify(); + } + "space" => { + this.query.push(' '); + this.selected_index = 0; + cx.notify(); + } + _ => { + if !event.keystroke.modifiers.control + && !event.keystroke.modifiers.alt + && !event.keystroke.modifiers.platform + { + if let Some(ch) = &event.keystroke.key_char { + this.query.push_str(ch); + this.selected_index = 0; + cx.notify(); + } else if key.chars().count() == 1 { + this.query.push_str(key); + this.selected_index = 0; + cx.notify(); + } + } + } + } + })) + .on_mouse_down( + gpui_kit::MouseButton::Left, + cx.listener(|_this, _event, _window, cx| { + cx.emit(SearchEverywhereEvent::Close); + }), + ) + .child( + // 居中模态卡片,宽 580px,深色卡片与边框阴影 + v_flex() + .id("search-everywhere-card") + .w(px(580.0)) + .max_h(px(480.0)) + .bg(ThemeColors::bg_sidebar()) + .border_1() + .border_color(ThemeColors::border()) + .rounded_lg() + .shadow_lg() + .overflow_hidden() + .on_mouse_down( + gpui_kit::MouseButton::Left, + cx.listener(|_this, _event, _window, cx| { + // 卡片内点击必须阻断冒泡,否则会触发遮罩的关闭逻辑。 + cx.stop_propagation(); + }), + ) + .child( + // 1. 顶部搜索框栏(含图标、输入展示、关闭按钮) + h_flex() + .h(px(46.0)) + .w_full() + .items_center() + .px_3() + .gap_2p5() + .border_b_1() + .border_color(ThemeColors::border()) + .bg(ThemeColors::bg_titlebar()) + .child( + Icon::new(IconName::Search) + .size(px(16.0)) + .text_color(ThemeColors::accent_blue()), + ) + .child( + h_flex() + .flex_1() + .items_center() + .gap_1() + .child( + div() + .text_sm() + .text_color(if self.query.is_empty() { + ThemeColors::text_muted() + } else { + ThemeColors::text_primary() + }) + .child(if self.query.is_empty() { + crate::i18n::menu_text( + cx, + "search.everywherePlaceholder", + ) + .to_string() + } else { + self.query.clone() + }), + ) + .child( + // 闪烁光标模拟 + div().w(px(2.0)).h(px(14.0)).bg(ThemeColors::accent_blue()), + ), + ) + .when(!self.query.is_empty(), |row| { + row.child( + Button::new("clear-search-query") + .small() + .ghost() + .icon(IconName::Close) + .on_click(cx.listener(|this, _event, _window, cx| { + this.query.clear(); + this.selected_index = 0; + cx.notify(); + })), + ) + }) + .child( + Button::new("close-search-modal") + .small() + .ghost() + .icon(IconName::Close) + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(SearchEverywhereEvent::Close); + })), + ), + ) + .child( + // 2. Scope 药丸选项(All, Files, Actions) + h_flex() + .h(px(34.0)) + .w_full() + .items_center() + .px_3() + .gap_1p5() + .border_b_1() + .border_color(ThemeColors::border()) + .bg(ThemeColors::bg_tab_bar()) + .child(self.render_scope_pill( + "scope-all", + crate::i18n::menu_text(cx, "search.scopeAll").to_string(), + self.scope == SearchScope::All, + SearchScope::All, + cx, + )) + .child(self.render_scope_pill( + "scope-files", + crate::i18n::menu_text(cx, "search.scopeFiles").to_string(), + self.scope == SearchScope::Files, + SearchScope::Files, + cx, + )) + .child(self.render_scope_pill( + "scope-actions", + crate::i18n::menu_text(cx, "search.scopeActions").to_string(), + self.scope == SearchScope::Actions, + SearchScope::Actions, + cx, + )), + ) + .child( + // 3. 结果列表展示区 + div() + .flex_1() + .w_full() + .max_h(px(320.0)) + .overflow_y_scrollbar() + .py_1() + .when(filtered.is_empty(), |list| { + list.child( + div() + .w_full() + .py_8() + .text_center() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(crate::i18n::menu_text(cx, "search.noResults")), + ) + }) + .children(filtered.into_iter().enumerate().map(|(idx, item)| { + let is_selected = idx == current_index; + let item_clone = item.clone(); + + h_flex() + .id(idx) + .h(px(34.0)) + .w_full() + .items_center() + .justify_between() + .px_3() + .cursor_pointer() + .when(is_selected, |row| { + row.bg(ThemeColors::subtle_selection()) + .border_l_2() + .border_color(ThemeColors::accent_blue()) + }) + .when(!is_selected, |row| { + row.hover(|h| h.bg(ThemeColors::bg_tab_hover())) + }) + .child(match &item { + MatchedItem::File { name, dir, .. } => { + let icon = if is_code_file(name) { + IconName::FileCode + } else { + IconName::FileText + }; + h_flex() + .items_center() + .gap_2() + .child( + Icon::new(icon) + .size(px(14.0)) + .text_color(ThemeColors::accent_blue()), + ) + .child( + div() + .text_xs() + .font_weight(FontWeight::MEDIUM) + .text_color(ThemeColors::text_primary()) + .child(name.clone()), + ) + .when(!dir.is_empty(), |row| { + row.child( + div() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(dir.clone()), + ) + }) + .into_any_element() + } + MatchedItem::Action { id, icon, .. } => h_flex() + .items_center() + .gap_2() + .child( + Icon::new(*icon) + .size(px(14.0)) + .text_color(ThemeColors::accent_green()), + ) + .child( + div() + .text_xs() + .font_weight(FontWeight::MEDIUM) + .text_color(ThemeColors::text_primary()) + .child(action_display_name(id, cx)), + ) + .into_any_element(), + }) + .child(match &item { + MatchedItem::File { .. } => div() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(crate::i18n::menu_text(cx, "menu.file")) + .into_any_element(), + MatchedItem::Action { shortcut, .. } => { + if let Some(sc) = shortcut { + div() + .px_1p5() + .py(px(1.0)) + .rounded_sm() + .bg(ThemeColors::bg_titlebar()) + .border_1() + .border_color(ThemeColors::border()) + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(sc.clone()) + .into_any_element() + } else { + div().into_any_element() + } + } + }) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.select_item(&item_clone, cx); + })) + })), + ) + .child( + // 4. 底部标题 + h_flex() + .h(px(26.0)) + .w_full() + .items_center() + .justify_end() + .px_3() + .bg(ThemeColors::bg_titlebar()) + .border_t_1() + .border_color(ThemeColors::border()) + .child( + div() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(crate::i18n::menu_text(cx, "workbench.search")), + ), + ), + ) + } +} + +impl SearchEverywhereModal { + fn render_scope_pill( + &self, + id: &'static str, + label: String, + is_active: bool, + scope: SearchScope, + cx: &mut Context, + ) -> impl IntoElement { + h_flex() + .id(id) + .items_center() + .px_2p5() + .py(px(2.0)) + .rounded_full() + .cursor_pointer() + .text_xs() + .when(is_active, |pill| { + pill.bg(ThemeColors::accent_blue()) + .text_color(ThemeColors::text_primary()) + .font_weight(FontWeight::BOLD) + }) + .when(!is_active, |pill| { + pill.text_color(ThemeColors::text_muted()).hover(|h| { + h.bg(ThemeColors::bg_tab_hover()) + .text_color(ThemeColors::text_primary()) + }) + }) + .child(label) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.scope = scope; + this.selected_index = 0; + cx.notify(); + })) + } +} diff --git a/linux/src/workbench/settings_dialog.rs b/linux/src/workbench/settings_dialog.rs new file mode 100644 index 000000000..7d2d1c5c1 --- /dev/null +++ b/linux/src/workbench/settings_dialog.rs @@ -0,0 +1,3159 @@ +//! 设置对话框:复刻 Tauri 端 `settings-dialog.tsx` 的 12 分类布局。 +//! +//! 面板尺寸对齐 Tauri 的 820×620:header 44、footer 44、左侧导航 190、右侧内容 +//! 滚动区。所有可持久化项都通过 [`crate::settings::update`] 写入 XDG JSON +//! (`$XDG_CONFIG_HOME/lithe/settings.json`);主题相关项写入后额外调用 +//! [`crate::settings::apply_theme`] 让工作台调色板立即生效。少量 Tauri 侧存在但 +//! Linux `Settings` 尚未覆盖的项(Vim、Git 视图、日志级别等)只作为对话框本地 +//! 占位状态,不落盘,待 `Settings` 补齐字段后再接入。 + +use gpui_kit::assets::IconName; +use gpui_kit::component::button::{Button, ButtonVariants as _}; +use gpui_kit::component::input::{Input, InputState, Textarea, TextareaState}; +use gpui_kit::component::menu::DropdownMenu as _; +use gpui_kit::component::scroll::ScrollableElement as _; +use gpui_kit::component::{h_flex, v_flex, Icon, Sizable as _}; +use gpui_kit::prelude::FluentBuilder as _; +use gpui_kit::{ + div, px, rgba, AppContext as _, Context, Entity, EventEmitter, FontWeight, + InteractiveElement as _, IntoElement, ParentElement as _, Render, + StatefulInteractiveElement as _, Styled as _, Window, +}; + +use crate::core::CoreClient; +use crate::settings::{self, Settings}; +use crate::theme::ThemeColors; +use crate::workbench::run::{ + default_generated_configuration_id, list_java_sources, parse_resolved_configurations, + read_toolchain_paths, write_generated_documents, write_toolchain_paths, ToolchainPaths, +}; + +/// 设置分类,顺序与 Tauri `categories` 数组一致。 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum SettingsCategory { + #[default] + General, + Project, + Run, + Editor, + Keyboard, + Terminal, + Lsp, + Ai, + AiCommit, + Git, + Logs, + Updates, +} + +impl SettingsCategory { + /// 全部分类,供左侧导航按固定顺序渲染。 + pub const ALL: [SettingsCategory; 12] = [ + SettingsCategory::General, + SettingsCategory::Project, + SettingsCategory::Run, + SettingsCategory::Editor, + SettingsCategory::Keyboard, + SettingsCategory::Terminal, + SettingsCategory::Lsp, + SettingsCategory::Ai, + SettingsCategory::AiCommit, + SettingsCategory::Git, + SettingsCategory::Logs, + SettingsCategory::Updates, + ]; + + /// 持久化用的 id,与 Tauri `MacSettingsCategory` 字符串对应。 + pub fn id(self) -> &'static str { + match self { + SettingsCategory::General => "general", + SettingsCategory::Project => "project", + SettingsCategory::Run => "run", + SettingsCategory::Editor => "editor", + SettingsCategory::Keyboard => "keyboard", + SettingsCategory::Terminal => "terminal", + SettingsCategory::Lsp => "lsp", + SettingsCategory::Ai => "ai", + SettingsCategory::AiCommit => "ai-commit", + SettingsCategory::Git => "git", + SettingsCategory::Logs => "logs", + SettingsCategory::Updates => "updates", + } + } + + /// 分类 i18n 键,与 Tauri `settings-dialog.tsx` 各分类 `labelKey` 逐一对应。 + pub fn title_key(self) -> &'static str { + match self { + SettingsCategory::General => "settings.tabs.general", + SettingsCategory::Project => "settings.project.title", + SettingsCategory::Run => "settings.run.title", + SettingsCategory::Editor => "settings.tabs.editor", + SettingsCategory::Keyboard => "settings.tabs.keyboard", + SettingsCategory::Terminal => "settings.tabs.terminal", + SettingsCategory::Lsp => "settings.tabs.lsp", + SettingsCategory::Ai => "settings.tabs.ai", + SettingsCategory::AiCommit => "settings.tabs.aiCommit", + SettingsCategory::Git => "settings.tabs.git", + SettingsCategory::Logs => "settings.tabs.logs", + SettingsCategory::Updates => "settings.tabs.updates", + } + } + + /// 导航图标;与 Tauri `settings-dialog.tsx` 各分类图标一一对应 + /// (GearSix→Settings、Gear→Cog、CodeBlock→Code、TerminalWindow→SquareTerminal、 + /// MagicWand→WandSparkles、ArrowClockwise→RotateCw)。 + pub fn icon(self) -> IconName { + match self { + SettingsCategory::General => IconName::Settings, + SettingsCategory::Project => IconName::Folder, + SettingsCategory::Run => IconName::Cog, + SettingsCategory::Editor => IconName::Code, + SettingsCategory::Keyboard => IconName::Keyboard, + SettingsCategory::Terminal => IconName::SquareTerminal, + SettingsCategory::Lsp => IconName::Database, + SettingsCategory::Ai => IconName::WandSparkles, + SettingsCategory::AiCommit => IconName::WandSparkles, + SettingsCategory::Git => IconName::Code, + SettingsCategory::Logs => IconName::FileText, + SettingsCategory::Updates => IconName::RotateCw, + } + } + + /// 由持久化的 `lastSettingsTab` 反解分类;`language` 归到 LSP,未知值回退常规。 + pub fn from_id(id: &str) -> Self { + match id { + "general" => SettingsCategory::General, + "project" => SettingsCategory::Project, + "run" => SettingsCategory::Run, + "editor" => SettingsCategory::Editor, + "keyboard" => SettingsCategory::Keyboard, + "terminal" => SettingsCategory::Terminal, + "lsp" | "language" => SettingsCategory::Lsp, + "ai" => SettingsCategory::Ai, + "ai-commit" => SettingsCategory::AiCommit, + "git" => SettingsCategory::Git, + "logs" => SettingsCategory::Logs, + "updates" => SettingsCategory::Updates, + _ => SettingsCategory::General, + } + } +} + +/// 对话框对外事件:关闭、普通设置变更,以及 Run 文档/工具链变更。 +#[derive(Debug, Clone)] +pub enum SettingsEvent { + Close, + Changed, + RunConfigurationChanged, +} + +/// 居中设置面板(宽 820px,高 620px)。 +pub struct SettingsDialog { + /// 当前激活分类 + pub active_category: SettingsCategory, + /// 当前工作区根路径,Project 分类只读展示 + workspace_root: String, + /// Git 本地更改保护策略(对齐 Tauri `GeneralPanel` 的本地 `gitPolicy` state,暂不落盘) + git_policy: String, + /// 本次会话诊断日志开关(对齐 Tauri 日志面板的会话级状态,重启恢复默认) + diagnostic_mode: bool, + /// 隐藏目录/文件模式多行编辑器(General 分组,内容即设置值,应用时落盘)。 + hidden_dirs_input: Option>, + hidden_files_input: Option>, + /// 项目工具链单行输入(Project 分组,打开分类时由 local.json 回填)。 + project_jdk_input: Option>, + project_maven_input: Option>, + project_maven_jdk_input: Option>, + /// 已加载工具链的工作区(避免跨项目复用脏输入框)。 + project_loaded_for: String, + /// 项目/运行/日志/更新分组的操作回执展示。 + project_status: String, + run_configs: Vec, + run_status: String, + run_load_seq: u64, + client: CoreClient, + /// AI 模型单行输入(provider 切换不重置,由用户显式修改)。 + ai_model_input: Option>, + /// Git 可执行路径单行输入。 + git_exe_input: Option>, + /// 日志自定义目录单行输入。 + log_dir_input: Option>, + logs_status: String, + updates_status: String, +} + +impl EventEmitter for SettingsDialog {} + +impl SettingsDialog { + /// 从持久化设置读取上次打开的分类;同时缓存工作区路径供 Project 分类展示。 + pub fn new(cx: &mut Context) -> Self { + let active_category = SettingsCategory::from_id(&settings::get(cx).last_settings_tab); + let workspace_root = std::env::current_dir() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + + Self { + active_category, + workspace_root, + git_policy: "ask".to_string(), + diagnostic_mode: false, + hidden_dirs_input: None, + hidden_files_input: None, + project_jdk_input: None, + project_maven_input: None, + project_maven_jdk_input: None, + project_loaded_for: String::new(), + project_status: String::new(), + run_configs: Vec::new(), + run_status: String::new(), + run_load_seq: 0, + client: CoreClient::new(), + ai_model_input: None, + git_exe_input: None, + log_dir_input: None, + logs_status: String::new(), + updates_status: String::new(), + } + } + + /// 切换工作区并使旧的 Run 配置加载结果失效。 + pub fn set_workspace_root(&mut self, root: String, cx: &mut Context) { + if self.workspace_root == root { + return; + } + self.workspace_root = root; + self.run_load_seq += 1; + self.run_configs.clear(); + self.run_status.clear(); + self.project_loaded_for.clear(); + self.project_status.clear(); + cx.notify(); + } + + /// 切换分类并把 `lastSettingsTab` 写回磁盘,保证下次打开停在同一页。 + pub fn set_category(&mut self, cat: SettingsCategory, cx: &mut Context) { + self.active_category = cat; + settings::update(cx, |s| s.last_settings_tab = cat.id().to_string()); + if cat == SettingsCategory::Run { + self.refresh_run_configs(false, cx); + } + cx.notify(); + } + + /// 打开对话框时按持久化的 `lastSettingsTab` 重置分类。 + pub fn open(&mut self, cx: &mut Context) { + self.active_category = SettingsCategory::from_id(&settings::get(cx).last_settings_tab); + if self.active_category == SettingsCategory::Run { + self.refresh_run_configs(false, cx); + } + cx.notify(); + } + + fn refresh_run_configs(&mut self, generate: bool, cx: &mut Context) { + self.run_load_seq += 1; + let seq = self.run_load_seq; + let root = self.workspace_root.clone(); + let client = self.client.clone(); + self.run_status = if generate { + "生成中…".to_string() + } else { + "加载中…".to_string() + }; + cx.notify(); + cx.spawn(async move |this, cx| { + let result: Result, String> = async { + if generate { + let generated = client + .execute::( + &cx, + "runConfig.generate", + serde_json::json!({ + "root": root, + "paths": list_java_sources(&root), + "modulePaths": [] + }), + ) + .await?; + if !this + .update(cx, |view, _cx| view.run_load_seq == seq) + .unwrap_or(false) + { + return Err("stale run configuration request".to_string()); + } + let document = generated + .get("generated") + .cloned() + .unwrap_or_else(|| serde_json::json!({"version": 2, "configurations": []})); + let requirements = generated + .get("toolchainRequirements") + .cloned() + .unwrap_or_else(|| serde_json::json!({"version": 1, "toolchains": {}})); + write_generated_documents( + &root, + &document, + &requirements, + default_generated_configuration_id(&document).as_deref(), + )?; + } + let inspection = client + .execute::( + &cx, + "runConfig.inspect", + serde_json::json!({ "root": root, "checkFingerprint": true }), + ) + .await?; + if inspection.get("status").and_then(serde_json::Value::as_str) != Some("ready") { + return Err("Run configuration has not been generated".to_string()); + } + let resolved = client + .execute::( + &cx, + "runConfig.resolve", + serde_json::json!({ "root": root, "toolchainCandidates": [] }), + ) + .await?; + let parsed = parse_resolved_configurations(&resolved)?; + Ok(parsed + .configurations + .into_iter() + .map(|item| format!("{} ({})", item.name, item.provider)) + .collect()) + } + .await; + let _ = this.update(cx, |view, cx| { + if view.run_load_seq != seq { + return; + } + match result { + Ok(configs) => { + view.run_configs = configs; + view.run_status = if view.run_configs.is_empty() { + "暂无可运行配置".to_string() + } else { + String::new() + }; + if generate { + cx.emit(SettingsEvent::RunConfigurationChanged); + } + } + Err(error) => view.run_status = error, + } + cx.notify(); + }); + }) + .detach(); + } + + /// 恢复本地占位项的默认值(不涉及持久化)。 + fn reset_placeholders(&mut self) { + self.git_policy = "ask".to_string(); + self.diagnostic_mode = false; + self.hidden_dirs_input = None; + self.hidden_files_input = None; + self.project_jdk_input = None; + self.project_maven_input = None; + self.project_maven_jdk_input = None; + self.project_loaded_for = String::new(); + self.project_status = String::new(); + self.run_configs = Vec::new(); + self.run_status = String::new(); + self.run_load_seq = self.run_load_seq.saturating_add(1); + self.ai_model_input = None; + self.git_exe_input = None; + self.log_dir_input = None; + self.logs_status = String::new(); + self.updates_status = String::new(); + } + + /// 写入设置、广播 `Changed` 并刷新视图。 + fn commit(&self, cx: &mut Context, mutate: impl FnOnce(&mut Settings)) { + settings::update(cx, mutate); + cx.emit(SettingsEvent::Changed); + cx.notify(); + } + + /// 写入主题相关设置后,立即把解析后的主题应用到工作台调色板。 + fn commit_theme(&self, cx: &mut Context, mutate: impl FnOnce(&mut Settings)) { + settings::update(cx, mutate); + let theme_id = settings::resolved_theme_id(settings::get(cx), false); + settings::apply_theme(&theme_id); + cx.emit(SettingsEvent::Changed); + cx.notify(); + } + + /// 懒创建单行输入框(首次渲染时用初始值填充,后续渲染复用用户编辑态)。 + fn ensure_input( + slot: &mut Option>, + initial: &str, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + if let Some(entity) = slot.clone() { + return entity; + } + let initial = initial.to_string(); + let entity = cx.new(|cx| InputState::new(window, cx).default_value(initial)); + *slot = Some(entity.clone()); + entity + } + + /// 懒创建多行输入框(隐藏路径模式编辑用)。 + fn ensure_textarea( + slot: &mut Option>, + initial: &str, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + if let Some(entity) = slot.clone() { + return entity; + } + let initial = initial.to_string(); + let entity = cx.new(|cx| TextareaState::new(window, cx).default_value(initial)); + *slot = Some(entity.clone()); + entity + } + + /// 单行文本输入渲染(宽 220px,对齐 Tauri `Input` 控件)。 + fn render_text_input(entity: Entity) -> impl IntoElement { + div() + .w(px(220.0)) + .child(Input::new(&entity).cleanable(true)) + } +} + +impl Render for SettingsDialog { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + // 全屏半透明遮罩:点击遮罩关闭,点击卡片不冒泡。 + div() + .id("settings-dialog-backdrop") + .absolute() + .inset_0() + .bg(rgba(0x00000088)) + .flex() + .items_center() + .justify_center() + .on_mouse_down( + gpui_kit::MouseButton::Left, + cx.listener(|_this, _event, _window, cx| { + cx.emit(SettingsEvent::Close); + }), + ) + .child( + v_flex() + .id("settings-dialog-card") + .w(px(820.0)) + .h(px(620.0)) + .bg(ThemeColors::surface()) + .border_1() + .border_color(ThemeColors::border()) + .rounded_lg() + .shadow_lg() + .overflow_hidden() + .on_mouse_down( + gpui_kit::MouseButton::Left, + cx.listener(|_this, _event, _window, cx| { + // 卡片内点击必须阻断冒泡,否则会触发遮罩的关闭逻辑。 + cx.stop_propagation(); + }), + ) + // 1. 头部:标题、搜索占位、关闭按钮 + .child( + h_flex() + .h(px(44.0)) + .w_full() + .items_center() + .justify_between() + .gap_3() + .px_3() + .border_b_1() + .border_color(ThemeColors::border()) + .bg(ThemeColors::surface()) + .child( + h_flex() + .items_center() + .gap_2() + .child( + Icon::new(IconName::Settings) + .size(px(16.0)) + .text_color(ThemeColors::primary()), + ) + .child( + div() + .text_sm() + .font_weight(FontWeight::BOLD) + .text_color(ThemeColors::foreground()) + .child(crate::i18n::menu_text( + cx, + "workbench.settings", + )), + ), + ) + .child( + // 搜索输入框占位:仅展示,不接输入。 + h_flex() + .w(px(220.0)) + .h(px(28.0)) + .items_center() + .gap_2() + .px_2p5() + .rounded_sm() + .border_1() + .border_color(ThemeColors::border()) + .bg(ThemeColors::background()) + .child( + Icon::new(IconName::Search) + .size(px(13.0)) + .text_color(ThemeColors::subtle_foreground()), + ) + .child( + div() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child( + crate::i18n::menu_text(cx, "settings.search") + .to_string(), + ), + ), + ) + .child( + Button::new("settings-close") + .small() + .ghost() + .icon(IconName::Close) + .tooltip(crate::i18n::menu_text(cx, "ui.close").to_string()) + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(SettingsEvent::Close); + })), + ), + ) + // 2. 主体:左侧分类导航 + 右侧滚动内容 + .child( + h_flex() + .flex_1() + .w_full() + .min_h_0() + .overflow_hidden() + .child( + v_flex() + .w(px(190.0)) + .h_full() + .flex_shrink_0() + .gap_0p5() + .p_2() + .border_r_1() + .border_color(ThemeColors::border()) + .bg(ThemeColors::surface()) + .children( + SettingsCategory::ALL + .iter() + .map(|cat| self.render_nav_item(*cat, cx)), + ), + ) + .child( + div() + .flex_1() + .h_full() + .min_w_0() + .bg(ThemeColors::background()) + .p_4() + .overflow_y_scrollbar() + .child( + v_flex() + .w_full() + .gap_4() + .child( + div() + .text_lg() + .font_weight(FontWeight::SEMIBOLD) + .text_color(ThemeColors::foreground()) + .child(crate::i18n::menu_text( + cx, + self.active_category.title_key(), + )), + ) + .child(match self.active_category { + SettingsCategory::General => self + .render_general_content(window, cx) + .into_any_element(), + SettingsCategory::Project => self + .render_project_content(window, cx) + .into_any_element(), + SettingsCategory::Run => { + self.render_run_content(cx).into_any_element() + } + SettingsCategory::Editor => self + .render_editor_content(cx) + .into_any_element(), + SettingsCategory::Keyboard => self + .render_keyboard_content(cx) + .into_any_element(), + SettingsCategory::Terminal => self + .render_terminal_content(cx) + .into_any_element(), + SettingsCategory::Lsp => { + self.render_lsp_content(cx).into_any_element() + } + SettingsCategory::Ai => self + .render_ai_content(window, cx) + .into_any_element(), + SettingsCategory::AiCommit => self + .render_ai_commit_content(cx) + .into_any_element(), + SettingsCategory::Git => self + .render_git_content(window, cx) + .into_any_element(), + SettingsCategory::Logs => self + .render_logs_content(window, cx) + .into_any_element(), + SettingsCategory::Updates => self + .render_updates_content(cx) + .into_any_element(), + }), + ), + ), + ) + // 3. 底部:左侧恢复默认,右侧主按钮关闭 + .child( + h_flex() + .h(px(44.0)) + .w_full() + .items_center() + .justify_between() + .px_3() + .border_t_1() + .border_color(ThemeColors::border()) + .bg(ThemeColors::surface()) + .child( + Button::new("settings-restore-defaults") + .small() + .ghost() + .label(crate::i18n::menu_text( + cx, + "settings.mac.restoreDefaults", + )) + .on_click(cx.listener(|this, _event, _window, cx| { + // 恢复默认:整体替换为 Settings::default 并落盘。 + settings::update(cx, |s| *s = Settings::default()); + let theme_id = + settings::resolved_theme_id(settings::get(cx), false); + settings::apply_theme(&theme_id); + this.active_category = SettingsCategory::from_id( + &settings::get(cx).last_settings_tab, + ); + this.reset_placeholders(); + cx.emit(SettingsEvent::Changed); + cx.notify(); + })), + ) + .child( + Button::new("settings-done") + .small() + .primary() + .label( + crate::i18n::menu_text(cx, "settings.mac.done").to_string(), + ) + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(SettingsEvent::Close); + })), + ), + ), + ) + } +} + +/// 常规渲染块:分组、行、控件与分类内容。 +impl SettingsDialog { + /// 左侧导航项:active 使用 accent 背景 + 左侧 primary 竖条 + 加粗前景。 + fn render_nav_item(&self, cat: SettingsCategory, cx: &mut Context) -> impl IntoElement { + let is_active = self.active_category == cat; + h_flex() + .id(cat.id()) + .h(px(32.0)) + .w_full() + .items_center() + .gap_2p5() + .px_2p5() + .rounded_sm() + .border_l_2() + .cursor_pointer() + .text_xs() + .when(is_active, |row| { + row.bg(ThemeColors::accent()) + .border_color(ThemeColors::primary()) + .text_color(ThemeColors::foreground()) + .font_weight(FontWeight::BOLD) + }) + .when(!is_active, |row| { + row.border_color(rgba(0x00000000)) + .text_color(ThemeColors::subtle_foreground()) + .hover(|h| { + h.bg(ThemeColors::accent()) + .text_color(ThemeColors::foreground()) + }) + }) + .child( + Icon::new(cat.icon()) + .size(px(14.0)) + .text_color(if is_active { + ThemeColors::foreground() + } else { + ThemeColors::subtle_foreground() + }), + ) + .child(crate::i18n::menu_text(cx, cat.title_key())) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.set_category(cat, cx); + })) + } + + /// 分组容器:标题条 + 内容区,对应 Tauri `SettingsGroup`。 + fn render_group(&self, title: String, content: impl IntoElement) -> impl IntoElement { + v_flex() + .w_full() + .rounded_md() + .border_1() + .border_color(ThemeColors::border()) + .bg(ThemeColors::surface()) + .overflow_hidden() + .child( + div() + .px_3() + .py_2() + .border_b_1() + .border_color(ThemeColors::border()) + .text_xs() + .font_weight(FontWeight::MEDIUM) + .text_color(ThemeColors::subtle_foreground()) + .child(title), + ) + .child(v_flex().w_full().gap_3().p_3().child(content)) + } + + /// 设置行:左侧标签与描述,右侧控件。标签与描述使用 owned 字符串, + /// 以便展示动态值(主题名、路径、版本号等)。 + fn render_row( + &self, + label: String, + description: Option, + control: impl IntoElement, + ) -> impl IntoElement { + let mut info = v_flex().flex_1().min_w_0().child( + div() + .text_xs() + .text_color(ThemeColors::foreground()) + .child(label), + ); + if let Some(desc) = description { + info = info.child( + div() + .mt_1() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child(desc), + ); + } + h_flex() + .w_full() + .min_h(px(32.0)) + .items_center() + .gap_4() + .child(info) + .child(div().flex_shrink_0().child(control)) + } + + /// 开关控件:可点击药丸,圆点指示开关状态(对齐 Tauri Switch 无文字)。 + fn render_toggle( + &self, + id: &'static str, + value: bool, + cx: &mut Context, + toggle: impl Fn(&mut Self, &mut Context) + 'static, + ) -> impl IntoElement { + h_flex() + .id(id) + .items_center() + .gap_1p5() + .px_2p5() + .py_1() + .rounded_sm() + .border_1() + .cursor_pointer() + .text_xs() + .when(value, |el| { + el.bg(ThemeColors::primary()) + .border_color(ThemeColors::primary()) + .text_color(ThemeColors::foreground()) + }) + .when(!value, |el| { + el.bg(ThemeColors::background()) + .border_color(ThemeColors::border()) + .text_color(ThemeColors::subtle_foreground()) + .hover(|h| { + h.bg(ThemeColors::accent()) + .text_color(ThemeColors::foreground()) + }) + }) + .child(div().w(px(8.0)).h(px(8.0)).rounded_full().bg(if value { + ThemeColors::foreground() + } else { + ThemeColors::subtle_foreground() + })) + .on_click(cx.listener(move |this, _event, _window, cx| toggle(this, cx))) + } + + /// 数值步进器:减号 + 当前值 + 加号。 + fn render_stepper( + &self, + dec_id: &'static str, + inc_id: &'static str, + value: String, + cx: &mut Context, + on_dec: impl Fn(&mut Self, &mut Context) + 'static, + on_inc: impl Fn(&mut Self, &mut Context) + 'static, + ) -> impl IntoElement { + h_flex() + .items_center() + .rounded_sm() + .border_1() + .border_color(ThemeColors::border()) + .bg(ThemeColors::background()) + .overflow_hidden() + .child( + Button::new(dec_id) + .small() + .ghost() + .icon(IconName::Minus) + .on_click(cx.listener(move |this, _event, _window, cx| on_dec(this, cx))), + ) + .child( + div() + .w(px(56.0)) + .text_center() + .text_xs() + .text_color(ThemeColors::foreground()) + .child(value), + ) + .child( + Button::new(inc_id) + .small() + .ghost() + .icon(IconName::Plus) + .on_click(cx.listener(move |this, _event, _window, cx| on_inc(this, cx))), + ) + } + + /// 只读文本值。 + fn render_value(&self, value: String) -> impl IntoElement { + div() + .px_2p5() + .py_1() + .rounded_sm() + .border_1() + .border_color(ThemeColors::border()) + .bg(ThemeColors::background()) + .text_xs() + .text_color(ThemeColors::muted_foreground()) + .child(value) + } + + /// 下拉选择框:当前值按钮 + ChevronDown,点击展开选项列表。 + /// 对齐 Tauri 各面板的原生 `