From 634ed5235e2c43a79a81b5eff0e933736cb1a4a4 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Wed, 23 Sep 2026 23:44:15 +0800 Subject: [PATCH 01/59] =?UTF-8?q?feat(linux):=20=E6=8E=A5=E5=85=A5=20GPUI?= =?UTF-8?q?=20Kit=20=E5=AE=9E=E7=8E=B0=20Linux=20=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E4=B8=8E=E6=9E=84=E5=BB=BA=E6=B5=81=E6=B0=B4=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 基于 gpui-kit (0.6) 建立独立 Linux 桌面工程 linux/ - 实现 CoreClient 调度层对接 rust/lithe-core - 构建工作台基础视图:顶部工具栏、侧边栏(文件/搜索/Git)、多标签代码区与 PTY 原生终端底栏 - 新增 Linux 构建脚本 scripts/build-linux.sh 与打包脚本 scripts/package-linux.sh - 新增 Linux CI 工作流 .github/workflows/ci-linux.yml - 更新 .gitignore 排除目标产物与临时文件 --- .github/workflows/ci-linux.yml | 80 + .gitignore | 9 +- linux/Cargo.lock | 9629 +++++++++++++++++++++++++++ linux/Cargo.toml | 14 + linux/src/core/client.rs | 272 + linux/src/core/mod.rs | 3 + linux/src/lib.rs | 4 + linux/src/main.rs | 20 + linux/src/workbench/bottom_panel.rs | 197 + linux/src/workbench/editor.rs | 296 + linux/src/workbench/mod.rs | 18 + linux/src/workbench/sidebar.rs | 622 ++ linux/src/workbench/terminal.rs | 235 + linux/src/workbench/toolbar.rs | 126 + linux/src/workbench/view.rs | 162 + scripts/build-linux.sh | 65 + scripts/package-linux.sh | 78 + 17 files changed, 11829 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci-linux.yml create mode 100644 linux/Cargo.lock create mode 100644 linux/Cargo.toml create mode 100644 linux/src/core/client.rs create mode 100644 linux/src/core/mod.rs create mode 100644 linux/src/lib.rs create mode 100644 linux/src/main.rs create mode 100644 linux/src/workbench/bottom_panel.rs create mode 100644 linux/src/workbench/editor.rs create mode 100644 linux/src/workbench/mod.rs create mode 100644 linux/src/workbench/sidebar.rs create mode 100644 linux/src/workbench/terminal.rs create mode 100644 linux/src/workbench/toolbar.rs create mode 100644 linux/src/workbench/view.rs create mode 100755 scripts/build-linux.sh create mode 100755 scripts/package-linux.sh 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..38786b01b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ .DS_Store .build/ -rust/target/ +target/ .swiftpm/ .idea/ +.vscode/ DerivedData/ /windows/build*/ dist/ @@ -16,3 +17,9 @@ docs/visual-qa/ reasonix.toml .codex/ /node_modules/ +*.swp +*.swo +*~ +*.tar.gz +*.deb +*.AppImage diff --git a/linux/Cargo.lock b/linux/Cargo.lock new file mode 100644 index 000000000..4732d81ce --- /dev/null +++ b/linux/Cargo.lock @@ -0,0 +1,9629 @@ +# 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 = "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.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" + +[[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.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" +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 = "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", + "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", + "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", + "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 = "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 = [ + "anyhow", + "gpui-kit", + "lithe-core", + "portable-pty", + "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 = "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", + "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 = "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 = "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-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.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "serde_json", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-java" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa6cbcdc8c679b214e616fd3300da67da0e492e066df01bcf5a5921a71e90d6" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0d1bf6fdd806e43ae5198f82f527056d359def39e54e67a0f478ac09dac081" + +[[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", +] + +[[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 = "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.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", + "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..b6aa6d589 --- /dev/null +++ b/linux/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "lithe-linux" +version = "0.1.0" +edition = "2021" + +[dependencies] +gpui-kit = "0.6" +lithe-core = { path = "../rust/lithe-core" } +portable-pty = "0.9" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["sync", "macros", "rt"] } +anyhow = "1" +uuid = { version = "1", features = ["v4"] } 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/lib.rs b/linux/src/lib.rs new file mode 100644 index 000000000..7c4fe1ee7 --- /dev/null +++ b/linux/src/lib.rs @@ -0,0 +1,4 @@ +pub mod core; +pub mod workbench; + +pub use workbench::WorkbenchView; diff --git a/linux/src/main.rs b/linux/src/main.rs new file mode 100644 index 000000000..69c77ca8e --- /dev/null +++ b/linux/src/main.rs @@ -0,0 +1,20 @@ +mod core; +mod workbench; + +use gpui_kit::component::Root; +use gpui_kit::*; +use workbench::WorkbenchView; + +fn main() { + let app = gpui_kit::application().with_assets(gpui_kit::assets::Assets); + + app.run(move |cx| { + gpui_kit::init(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/workbench/bottom_panel.rs b/linux/src/workbench/bottom_panel.rs new file mode 100644 index 000000000..4a5406f2a --- /dev/null +++ b/linux/src/workbench/bottom_panel.rs @@ -0,0 +1,197 @@ +use gpui_kit::component::button::{Button, ButtonVariants as _}; +use gpui_kit::component::scroll::ScrollableElement as _; +use gpui_kit::component::{h_flex, v_flex, Sizable as _}; +use gpui_kit::prelude::FluentBuilder as _; +use gpui_kit::{ + div, px, rgb, AppContext as _, Context, Entity, IntoElement, + ParentElement as _, Render, Styled as _, Window, +}; + +use crate::workbench::terminal::TerminalView; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BottomTab { + Terminal, + Output, + Problems, +} + +pub struct BottomPanelView { + pub active_tab: BottomTab, + pub is_collapsed: bool, + pub terminal: Entity, + pub output_logs: Vec, + pub problems: Vec, +} + +impl BottomPanelView { + pub fn new(working_dir: String, cx: &mut Context) -> Self { + let terminal = cx.new(|cx| TerminalView::new(working_dir, cx)); + + Self { + active_tab: BottomTab::Terminal, + is_collapsed: false, + terminal, + output_logs: vec!["[Lithe Linux] Initialized.".to_string()], + problems: Vec::new(), + } + } + + pub fn set_tab(&mut self, tab: BottomTab, cx: &mut Context) { + self.active_tab = tab; + self.is_collapsed = false; + cx.notify(); + } + + pub fn toggle_collapsed(&mut self, cx: &mut Context) { + self.is_collapsed = !self.is_collapsed; + cx.notify(); + } + + pub fn append_log(&mut self, log: String, cx: &mut Context) { + self.output_logs.push(log); + cx.notify(); + } +} + +impl Render for BottomPanelView { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + if self.is_collapsed { + return div() + .h(px(28.0)) + .w_full() + .bg(rgb(0x141522)) + .border_t_1() + .border_color(rgb(0x23263b)) + .flex() + .items_center() + .justify_between() + .px_3() + .child( + div() + .text_xs() + .text_color(rgb(0x8a90a2)) + .child("Panel collapsed"), + ) + .child( + Button::new("expand-panel") + .small() + .ghost() + .label("▲ Expand") + .on_click(cx.listener(|this, _event, _window, cx| { + this.toggle_collapsed(cx); + })), + ); + } + + v_flex() + .h(px(240.0)) + .w_full() + .bg(rgb(0x11121d)) + .border_t_1() + .border_color(rgb(0x23263b)) + .child( + // 顶部 Tab 切换栏 + h_flex() + .h(px(32.0)) + .w_full() + .bg(rgb(0x141522)) + .border_b_1() + .border_color(rgb(0x23263b)) + .items_center() + .justify_between() + .px_2() + .child( + h_flex() + .items_center() + .gap_1() + .child( + Button::new("tab-terminal") + .small() + .when(self.active_tab == BottomTab::Terminal, |btn| { + btn.primary() + }) + .when(self.active_tab != BottomTab::Terminal, |btn| { + btn.ghost() + }) + .label("Terminal") + .on_click(cx.listener(|this, _event, _window, cx| { + this.set_tab(BottomTab::Terminal, cx); + })), + ) + .child( + Button::new("tab-output") + .small() + .when(self.active_tab == BottomTab::Output, |btn| { + btn.primary() + }) + .when(self.active_tab != BottomTab::Output, |btn| { + btn.ghost() + }) + .label("Output") + .on_click(cx.listener(|this, _event, _window, cx| { + this.set_tab(BottomTab::Output, cx); + })), + ) + .child( + Button::new("tab-problems") + .small() + .when(self.active_tab == BottomTab::Problems, |btn| { + btn.primary() + }) + .when(self.active_tab != BottomTab::Problems, |btn| { + btn.ghost() + }) + .label(format!("Problems ({})", self.problems.len())) + .on_click(cx.listener(|this, _event, _window, cx| { + this.set_tab(BottomTab::Problems, cx); + })), + ), + ) + .child( + Button::new("collapse-panel") + .small() + .ghost() + .label("▼ Hide") + .on_click(cx.listener(|this, _event, _window, cx| { + this.toggle_collapsed(cx); + })), + ), + ) + .child( + // 内容区域根据 Tab 切换 + div().flex_1().w_full().child(match self.active_tab { + BottomTab::Terminal => div() + .size_full() + .child(self.terminal.clone()) + .into_any_element(), + BottomTab::Output => div() + .size_full() + .p_3() + .overflow_y_scrollbar() + .text_xs() + .font_family("monospace") + .text_color(rgb(0x9ca3af)) + .children( + self.output_logs + .iter() + .cloned() + .map(|log| div().child(log)), + ) + .into_any_element(), + BottomTab::Problems => div() + .size_full() + .p_3() + .overflow_y_scrollbar() + .text_xs() + .text_color(rgb(0x9ca3af)) + .child(if self.problems.is_empty() { + "No problems have been detected in the workspace.".to_string() + } else { + format!("{} problems found", self.problems.len()) + }) + .into_any_element(), + }), + ) + } +} diff --git a/linux/src/workbench/editor.rs b/linux/src/workbench/editor.rs new file mode 100644 index 000000000..09f450427 --- /dev/null +++ b/linux/src/workbench/editor.rs @@ -0,0 +1,296 @@ +use gpui_kit::component::scroll::ScrollableElement as _; +use gpui_kit::component::{h_flex, v_flex}; +use gpui_kit::prelude::FluentBuilder as _; +use gpui_kit::{ + div, px, rgb, Context, InteractiveElement as _, IntoElement, ParentElement as _, + Render, StatefulInteractiveElement as _, Styled as _, Window, +}; + +use crate::core::CoreClient; + +#[derive(Debug, Clone)] +pub struct EditorTab { + pub path: String, + pub title: String, + pub content: String, + pub is_dirty: bool, + pub cursor_line: usize, + pub cursor_col: usize, +} + +/// 多标签代码编辑器组件 +pub struct EditorView { + pub workspace_root: String, + pub tabs: Vec, + pub active_tab_index: Option, + #[allow(dead_code)] + pub encoding: String, + client: CoreClient, +} + +impl EditorView { + pub fn new(workspace_root: String, _cx: &mut Context) -> Self { + Self { + workspace_root, + tabs: Vec::new(), + active_tab_index: None, + encoding: "UTF-8".to_string(), + client: CoreClient::new(), + } + } + + /// 打开文件,如果已在标签中则切换,否则新增标签 + 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) { + self.active_tab_index = Some(pos); + 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, + cursor_line: 1, + cursor_col: 1, + }); + + self.active_tab_index = Some(self.tabs.len() - 1); + cx.notify(); + } + + /// 关闭指定索引的标签页 + pub fn close_tab(&mut self, index: usize, cx: &mut Context) { + if index < self.tabs.len() { + 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); + } + } + cx.notify(); + } + } + + /// 保存当前活动的标签页至磁盘(对接 `file.write`) + pub fn save_active(&mut self, cx: &mut Context) { + let Some(idx) = self.active_tab_index else { + return; + }; + let Some(tab) = self.tabs.get(idx) else { + return; + }; + + let root = self.workspace_root.clone(); + let path = tab.path.clone(); + let text = tab.content.clone(); + let client = self.client.clone(); + + cx.spawn(async move |this, cx| { + let task = client.write_file(&cx, &root, &path, &text); + if task.await.is_ok() { + let _ = this.update(cx, |ed, cx| { + if let Some(active_idx) = ed.active_tab_index { + if let Some(t) = ed.tabs.get_mut(active_idx) { + if t.path == path { + t.is_dirty = false; + cx.notify(); + } + } + } + }); + } + }) + .detach(); + } + + /// 获取推断的代码语言名称 + 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 { + "Plain Text" + } + } +} + +impl Render for EditorView { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let active_tab = self + .active_tab_index + .and_then(|idx| self.tabs.get(idx).cloned()); + + v_flex() + .size_full() + .bg(rgb(0x13141f)) + .child( + // 1. 顶部标签栏(Tab Bar) + h_flex() + .h(px(36.0)) + .w_full() + .bg(rgb(0x10111a)) + .border_b_1() + .border_color(rgb(0x23263b)) + .items_center() + .overflow_x_scrollbar() + .px_1() + .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; + + h_flex() + .id(idx) + .h(px(32.0)) + .items_center() + .gap_2() + .px_3() + .rounded_t_sm() + .cursor_pointer() + .when(is_active, |t| { + t.bg(rgb(0x181926)).border_t_2().border_color(rgb(0x3b82f6)) + }) + .when(!is_active, |t| { + t.bg(rgb(0x10111a)).hover(|h| h.bg(rgb(0x151624))) + }) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.active_tab_index = Some(idx); + cx.notify(); + })) + .child( + div() + .text_xs() + .text_color(if is_active { + rgb(0xffffff) + } else { + rgb(0x9ca3af) + }) + .child(title), + ) + .when(is_dirty, |t| { + t.child( + div() + .text_xs() + .text_color(rgb(0x3b82f6)) + .child("●"), + ) + }) + .child( + div() + .id(("close-tab", idx)) + .text_xs() + .text_color(rgb(0x6b7280)) + .hover(|h| h.text_color(rgb(0xef4444))) + .child("×") + .on_click(cx.listener(move |this, _event, _window, cx| { + this.close_tab(idx, cx); + })), + ) + })), + ) + .child( + // 2. 中央内容编辑区 + div().flex_1().w_full().overflow_scrollbar().child( + if let Some(tab) = &active_tab { + let lines: Vec = tab.content.lines().map(|s| s.to_string()).collect(); + + h_flex() + .size_full() + .p_3() + .gap_4() + .font_family("monospace") + .text_xs() + .child( + // 行号列 + v_flex() + .flex_shrink_0() + .text_color(rgb(0x4b5563)) + .children((1..=lines.len().max(1)).map(|num| { + div().h(px(20.0)).child(format!("{num:4}")) + })), + ) + .child( + // 代码文本列 + v_flex() + .flex_1() + .text_color(rgb(0xe5e7eb)) + .children(lines.into_iter().enumerate().map(|(idx, line)| { + div() + .id(idx) + .h(px(20.0)) + .child(if line.is_empty() { " ".to_string() } else { line }) + })), + ) + } else { + // 空状态 + h_flex() + .size_full() + .items_center() + .justify_center() + .text_sm() + .text_color(rgb(0x6b7280)) + .child("No file opened. Select a file from the sidebar.") + }, + ), + ) + .child( + // 3. 底部状态栏 + h_flex() + .h(px(24.0)) + .w_full() + .bg(rgb(0x10111a)) + .border_t_1() + .border_color(rgb(0x23263b)) + .items_center() + .justify_between() + .px_3() + .text_xs() + .text_color(rgb(0x6b7280)) + .child( + h_flex().items_center().gap_3().child(if let Some(tab) = &active_tab { + format!("Ln {}, Col {}", tab.cursor_line, tab.cursor_col) + } else { + "Ready".to_string() + }), + ) + .child( + h_flex() + .items_center() + .gap_3() + .child(if let Some(tab) = &active_tab { + Self::language_name(&tab.path) + } else { + "Plain Text" + }) + .child("UTF-8") + .child("Lithe Core"), + ), + ) + } +} diff --git a/linux/src/workbench/mod.rs b/linux/src/workbench/mod.rs new file mode 100644 index 000000000..a2031fb4c --- /dev/null +++ b/linux/src/workbench/mod.rs @@ -0,0 +1,18 @@ +pub mod bottom_panel; +pub mod editor; +pub mod sidebar; +pub mod terminal; +pub mod toolbar; +pub mod view; + +#[allow(unused_imports)] +pub use bottom_panel::BottomPanelView; +#[allow(unused_imports)] +pub use editor::EditorView; +#[allow(unused_imports)] +pub use sidebar::SidebarView; +#[allow(unused_imports)] +pub use terminal::TerminalView; +#[allow(unused_imports)] +pub use toolbar::ToolbarView; +pub use view::WorkbenchView; diff --git a/linux/src/workbench/sidebar.rs b/linux/src/workbench/sidebar.rs new file mode 100644 index 000000000..66c7af64e --- /dev/null +++ b/linux/src/workbench/sidebar.rs @@ -0,0 +1,622 @@ +use gpui_kit::component::button::{Button, ButtonVariants as _}; +use gpui_kit::component::{h_flex, v_flex, Sizable as _}; +use gpui_kit::prelude::FluentBuilder as _; +use gpui_kit::{ + div, px, rgb, Context, EventEmitter, InteractiveElement as _, IntoElement, + ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Window, +}; +use serde::{Deserialize, Serialize}; + +use crate::core::CoreClient; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SidebarTab { + Explorer, + Search, + Git, +} + +/// 侧边栏文件树节点,对接 `lithe-core` 的工作区快照数据模型 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FileEntry { + pub path: String, + pub name: String, + pub is_directory: bool, + #[serde(default)] + pub size: Option, + #[serde(default)] + pub children: Option>, + #[serde(default)] + pub is_expanded: bool, +} + +#[derive(Debug, Clone)] +pub struct FlatFileItem { + pub path: String, + pub name: String, + pub is_directory: bool, + #[allow(dead_code)] + pub size: Option, + pub depth: usize, + pub is_expanded: bool, + #[allow(dead_code)] + pub has_children: bool, +} + +impl FileEntry { + pub fn collect_visible(&self, depth: usize, output: &mut Vec) { + let has_children = self + .children + .as_ref() + .map_or(false, |items| !items.is_empty()); + + output.push(FlatFileItem { + path: self.path.clone(), + name: self.name.clone(), + is_directory: self.is_directory, + size: self.size, + depth, + is_expanded: self.is_expanded, + has_children, + }); + + if self.is_directory && self.is_expanded { + if let Some(children) = &self.children { + for child in children { + child.collect_visible(depth + 1, output); + } + } + } + } +} + +/// 侧边栏派发的事件 +#[derive(Debug, Clone)] +pub enum SidebarEvent { + OpenFile(String), +} + +#[derive(Debug, Clone)] +pub struct SearchResultItem { + pub path: String, + pub line: Option, + pub preview: Option, +} + +#[derive(Debug, Clone)] +pub struct GitChangeItem { + pub path: String, + pub status: String, + #[allow(dead_code)] + pub staged: bool, +} + +/// 侧边栏主视图组件 +pub struct SidebarView { + pub root_path: String, + pub active_tab: SidebarTab, + // 资源管理器状态 + pub root_node: Option, + pub selected_path: Option, + pub is_loading: bool, + pub error_message: Option, + // 搜索状态 + #[allow(dead_code)] + pub search_query: String, + pub search_results: Vec, + pub is_searching: bool, + // Git 状态 + pub git_branch: Option, + pub git_changes: Vec, + pub is_git_loading: bool, + client: CoreClient, +} + +impl EventEmitter for SidebarView {} + +impl SidebarView { + pub fn new(root_path: String, cx: &mut Context) -> Self { + let mut view = Self { + root_path, + active_tab: SidebarTab::Explorer, + root_node: None, + selected_path: None, + is_loading: false, + error_message: None, + search_query: String::new(), + search_results: Vec::new(), + is_searching: false, + git_branch: None, + git_changes: Vec::new(), + is_git_loading: false, + client: CoreClient::new(), + }; + + view.refresh(cx); + view.refresh_git(cx); + view + } + + pub fn set_tab(&mut self, tab: SidebarTab, cx: &mut Context) { + self.active_tab = tab; + cx.notify(); + } + + /// 对接 `lithe-core` 刷新工作区文件快照 + pub fn refresh(&mut self, cx: &mut Context) { + self.is_loading = true; + self.error_message = None; + cx.notify(); + + let client = self.client.clone(); + let root = self.root_path.clone(); + + cx.spawn(async move |this, cx| { + let task = client.snapshot(&cx, &root); + + match task.await { + Ok(val) => { + let mut node = val + .get("root") + .and_then(|r| serde_json::from_value::(r.clone()).ok()); + + if let Some(entry) = &mut node { + entry.is_expanded = true; + } + + let _ = this.update(cx, |sidebar, cx| { + sidebar.root_node = node; + sidebar.is_loading = false; + cx.notify(); + }); + } + Err(err) => { + let _ = this.update(cx, |sidebar, cx| { + sidebar.error_message = Some(err); + sidebar.is_loading = false; + cx.notify(); + }); + } + } + }) + .detach(); + } + + /// 执行搜索 + #[allow(dead_code)] + pub fn execute_search(&mut self, query: &str, cx: &mut Context) { + self.search_query = query.to_string(); + if query.trim().is_empty() { + self.search_results.clear(); + cx.notify(); + return; + } + + self.is_searching = true; + cx.notify(); + + let client = self.client.clone(); + let root = self.root_path.clone(); + let q = query.to_string(); + + cx.spawn(async move |this, cx| { + let task = client.search(&cx, &root, &q); + + match task.await { + Ok(matches) => { + let items: Vec = matches + .into_iter() + .map(|m| SearchResultItem { + path: m + .get("path") + .and_then(|p| p.as_str()) + .unwrap_or("") + .to_string(), + line: m + .get("line") + .and_then(|l| l.as_u64()) + .map(|l| l as usize), + preview: m + .get("preview") + .and_then(|p| p.as_str()) + .map(|s| s.to_string()), + }) + .collect(); + + let _ = this.update(cx, |sidebar, cx| { + sidebar.search_results = items; + sidebar.is_searching = false; + cx.notify(); + }); + } + Err(_) => { + let _ = this.update(cx, |sidebar, cx| { + sidebar.search_results.clear(); + sidebar.is_searching = false; + cx.notify(); + }); + } + } + }) + .detach(); + } + + /// 刷新 Git 状态 + pub fn refresh_git(&mut self, cx: &mut Context) { + self.is_git_loading = true; + cx.notify(); + + let client = self.client.clone(); + let root = self.root_path.clone(); + + cx.spawn(async move |this, cx| { + let task = client.git_status(&cx, &root); + + match task.await { + Ok(val) => { + let branch = val + .get("branch") + .and_then(|b| b.as_str()) + .map(|s| s.to_string()); + + let mut changes = Vec::new(); + if let Some(arr) = val.get("changes").and_then(|c| c.as_array()) { + for item in arr { + let path = item + .get("path") + .and_then(|p| p.as_str()) + .unwrap_or("") + .to_string(); + let status = item + .get("status") + .and_then(|s| s.as_str()) + .unwrap_or("M") + .to_string(); + let staged = item + .get("staged") + .and_then(|s| s.as_bool()) + .unwrap_or(false); + + changes.push(GitChangeItem { + path, + status, + staged, + }); + } + } + + let _ = this.update(cx, |sidebar, cx| { + sidebar.git_branch = branch; + sidebar.git_changes = changes; + sidebar.is_git_loading = false; + cx.notify(); + }); + } + Err(_) => { + let _ = this.update(cx, |sidebar, cx| { + sidebar.is_git_loading = false; + cx.notify(); + }); + } + } + }) + .detach(); + } + + pub fn toggle_directory(&mut self, path: &str, cx: &mut Context) { + if let Some(root) = &mut self.root_node { + Self::toggle_dir_in_entry(root, path); + cx.notify(); + } + } + + fn toggle_dir_in_entry(entry: &mut FileEntry, target_path: &str) -> bool { + if entry.path == target_path && entry.is_directory { + entry.is_expanded = !entry.is_expanded; + return true; + } + + if let Some(children) = &mut entry.children { + for child in children { + if Self::toggle_dir_in_entry(child, target_path) { + return true; + } + } + } + false + } +} + +impl Render for SidebarView { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + v_flex() + .size_full() + .bg(rgb(0x161724)) + .border_r_1() + .border_color(rgb(0x23263b)) + .child( + // 顶部视图切换栏 (Explorer / Search / Git) + h_flex() + .h(px(36.0)) + .w_full() + .bg(rgb(0x141522)) + .border_b_1() + .border_color(rgb(0x23263b)) + .items_center() + .justify_between() + .px_2() + .child( + h_flex() + .items_center() + .gap_1() + .child( + Button::new("tab-explorer") + .small() + .when(self.active_tab == SidebarTab::Explorer, |b| { + b.primary() + }) + .when(self.active_tab != SidebarTab::Explorer, |b| { + b.ghost() + }) + .label("Files") + .on_click(cx.listener(|this, _event, _window, cx| { + this.set_tab(SidebarTab::Explorer, cx); + })), + ) + .child( + Button::new("tab-search") + .small() + .when(self.active_tab == SidebarTab::Search, |b| { + b.primary() + }) + .when(self.active_tab != SidebarTab::Search, |b| { + b.ghost() + }) + .label("Search") + .on_click(cx.listener(|this, _event, _window, cx| { + this.set_tab(SidebarTab::Search, cx); + })), + ) + .child( + Button::new("tab-git") + .small() + .when(self.active_tab == SidebarTab::Git, |b| b.primary()) + .when(self.active_tab != SidebarTab::Git, |b| b.ghost()) + .label(format!("Git ({})", self.git_changes.len())) + .on_click(cx.listener(|this, _event, _window, cx| { + this.set_tab(SidebarTab::Git, cx); + })), + ), + ) + .child( + Button::new("refresh-btn") + .small() + .ghost() + .label("↻") + .on_click(cx.listener(|this, _event, _window, cx| { + this.refresh(cx); + this.refresh_git(cx); + })), + ), + ) + .child( + // 对应面板内容渲染 + div().flex_1().w_full().child( + match self.active_tab { + SidebarTab::Explorer => self.render_explorer(cx).into_any_element(), + SidebarTab::Search => self.render_search(cx).into_any_element(), + SidebarTab::Git => self.render_git(cx).into_any_element(), + }, + ), + ) + } +} + +impl SidebarView { + fn render_explorer(&self, cx: &mut Context) -> impl IntoElement { + let mut visible_items = Vec::new(); + if let Some(root) = &self.root_node { + root.collect_visible(0, &mut visible_items); + } + + v_flex() + .size_full() + .p_2() + .gap_1() + .when(self.is_loading, |this| { + this.child( + div() + .text_xs() + .text_color(rgb(0x8a90a2)) + .p_2() + .child("Loading workspace files..."), + ) + }) + .when_some(self.error_message.as_ref(), |this, err| { + this.child( + div() + .text_xs() + .text_color(rgb(0xef4444)) + .p_2() + .child(err.clone()), + ) + }) + .children(visible_items.into_iter().enumerate().map(|(idx, item)| { + let is_selected = self.selected_path.as_deref() == Some(&item.path); + let path_str = item.path.clone(); + let is_dir = item.is_directory; + let indent = item.depth as f32 * 12.0; + + h_flex() + .id(idx) + .h(px(24.0)) + .w_full() + .items_center() + .cursor_pointer() + .rounded_sm() + .pl(px(indent + 4.0)) + .pr_2() + .text_xs() + .when(is_selected, |row| { + row.bg(rgb(0x2a2d42)).text_color(rgb(0xffffff)) + }) + .when(!is_selected, |row| { + row.text_color(rgb(0xcfd3e0)).hover(|h| h.bg(rgb(0x1e2030))) + }) + .child(if is_dir { + if item.is_expanded { "📂 " } else { "📁 " } + } else { + "📄 " + }) + .child(item.name) + .on_click(cx.listener({ + let path = path_str.clone(); + move |this, _event, _window, cx| { + if is_dir { + this.toggle_directory(&path, cx); + } else { + this.selected_path = Some(path.clone()); + cx.emit(SidebarEvent::OpenFile(path.clone())); + cx.notify(); + } + } + })) + })) + } + + fn render_search(&self, cx: &mut Context) -> impl IntoElement { + v_flex() + .size_full() + .p_2() + .gap_2() + .child( + h_flex() + .h(px(28.0)) + .w_full() + .items_center() + .bg(rgb(0x1e2030)) + .rounded_sm() + .px_2() + .child( + div() + .text_xs() + .text_color(rgb(0x8a90a2)) + .child("Search files or text..."), + ), + ) + .when(self.is_searching, |this| { + this.child( + div() + .text_xs() + .text_color(rgb(0x8a90a2)) + .child("Searching..."), + ) + }) + .children( + self.search_results + .iter() + .enumerate() + .map(|(idx, res)| { + let path = res.path.clone(); + let line_info = res + .line + .map(|l| format!(":{}", l)) + .unwrap_or_default(); + let preview = res.preview.clone().unwrap_or_default(); + + v_flex() + .id(idx) + .p_1() + .rounded_sm() + .cursor_pointer() + .hover(|h| h.bg(rgb(0x1e2030))) + .child( + h_flex() + .items_center() + .gap_1() + .text_xs() + .text_color(rgb(0x60a5fa)) + .child(format!("{}{}", path, line_info)), + ) + .child( + div() + .text_xs() + .text_color(rgb(0x9ca3af)) + .child(preview), + ) + .on_click(cx.listener({ + let p = path.clone(); + move |this, _event, _window, cx| { + this.selected_path = Some(p.clone()); + cx.emit(SidebarEvent::OpenFile(p.clone())); + cx.notify(); + } + })) + }), + ) + } + + fn render_git(&self, cx: &mut Context) -> impl IntoElement { + let branch = self.git_branch.clone().unwrap_or_else(|| "DETACHED".to_string()); + + v_flex() + .size_full() + .p_2() + .gap_2() + .child( + h_flex() + .items_center() + .gap_2() + .pb_2() + .border_b_1() + .border_color(rgb(0x23263b)) + .text_xs() + .text_color(rgb(0x10b981)) + .child("⎇ Branch:") + .child(branch), + ) + .child( + div() + .text_xs() + .text_color(rgb(0x8a90a2)) + .child(format!("Changed Files ({})", self.git_changes.len())), + ) + .children(self.git_changes.iter().enumerate().map(|(idx, change)| { + let path = change.path.clone(); + let status = change.status.clone(); + + h_flex() + .id(idx) + .h(px(24.0)) + .w_full() + .items_center() + .justify_between() + .px_2() + .rounded_sm() + .cursor_pointer() + .hover(|h| h.bg(rgb(0x1e2030))) + .text_xs() + .child( + h_flex() + .items_center() + .gap_2() + .text_color(rgb(0xcfd3e0)) + .child(path.clone()), + ) + .child( + div() + .text_color(rgb(0xf59e0b)) + .child(status), + ) + .on_click(cx.listener({ + let p = path.clone(); + move |this, _event, _window, cx| { + this.selected_path = Some(p.clone()); + cx.emit(SidebarEvent::OpenFile(p.clone())); + cx.notify(); + } + })) + })) + } +} diff --git a/linux/src/workbench/terminal.rs b/linux/src/workbench/terminal.rs new file mode 100644 index 000000000..090eaa3cb --- /dev/null +++ b/linux/src/workbench/terminal.rs @@ -0,0 +1,235 @@ +use gpui_kit::component::button::{Button, ButtonVariants as _}; +use gpui_kit::component::scroll::ScrollableElement as _; +use gpui_kit::component::{h_flex, v_flex, Sizable as _}; +use gpui_kit::{ + div, px, rgb, Context, InteractiveElement as _, IntoElement, ParentElement as _, Render, + Styled as _, Window, +}; +use portable_pty::{native_pty_system, CommandBuilder, PtySize}; +use std::io::{Read, Write}; +use std::sync::{Arc, Mutex}; +use std::thread; + +/// Linux 原生 PTY 会话 +pub struct TerminalSession { + writer: Arc>>, + pub output_lines: Arc>>, + #[allow(dead_code)] + input_buffer: Arc>, +} + +impl TerminalSession { + pub fn new(cols: u16, rows: u16, working_dir: &str) -> anyhow::Result { + let pty_system = native_pty_system(); + let pair = pty_system.openpty(PtySize { + rows, + cols, + pixel_width: 0, + pixel_height: 0, + })?; + + let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string()); + let mut cmd = CommandBuilder::new(&shell); + cmd.cwd(working_dir); + + let _child = pair.slave.spawn_command(cmd)?; + let mut reader = pair.master.try_clone_reader()?; + let writer = pair.master.take_writer()?; + + let output_lines = Arc::new(Mutex::new(vec![format!( + "Lithe Terminal (PTY Session: {shell})" + )])); + let output_lines_clone = Arc::clone(&output_lines); + + thread::spawn(move || { + let mut buf = [0u8; 1024]; + let mut line_accumulator = String::new(); + + while let Ok(n) = reader.read(&mut buf) { + if n == 0 { + break; + } + let chunk = String::from_utf8_lossy(&buf[..n]); + for ch in chunk.chars() { + if ch == '\n' { + let mut lines = output_lines_clone.lock().unwrap(); + lines.push(line_accumulator.clone()); + if lines.len() > 1000 { + lines.remove(0); + } + line_accumulator.clear(); + } else if ch == '\r' { + // ignore carriage return + } else { + line_accumulator.push(ch); + } + } + if !line_accumulator.is_empty() { + let mut lines = output_lines_clone.lock().unwrap(); + if let Some(last) = lines.last_mut() { + *last = line_accumulator.clone(); + } + } + } + }); + + Ok(Self { + writer: Arc::new(Mutex::new(writer)), + output_lines, + input_buffer: Arc::new(Mutex::new(String::new())), + }) + } + + pub fn write_input(&self, input: &str) -> anyhow::Result<()> { + let mut writer = self.writer.lock().unwrap(); + writer.write_all(input.as_bytes())?; + writer.flush()?; + Ok(()) + } + + pub fn get_lines(&self) -> Vec { + self.output_lines.lock().unwrap().clone() + } +} + +/// 终端视图组件 +pub struct TerminalView { + pub session: Option, + pub current_input: String, + pub working_dir: String, +} + +impl TerminalView { + pub fn new(working_dir: String, _cx: &mut Context) -> Self { + let session = TerminalSession::new(120, 30, &working_dir).ok(); + + Self { + session, + current_input: String::new(), + working_dir, + } + } + + pub fn send_command(&mut self, cmd: &str, cx: &mut Context) { + if let Some(session) = &self.session { + let mut full = cmd.to_string(); + full.push('\n'); + let _ = session.write_input(&full); + } + self.current_input.clear(); + cx.notify(); + } + + pub fn clear(&mut self, cx: &mut Context) { + if let Some(session) = &self.session { + if let Ok(mut lines) = session.output_lines.lock() { + lines.clear(); + lines.push("Terminal cleared.".to_string()); + } + } + cx.notify(); + } +} + +impl Render for TerminalView { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let lines = self + .session + .as_ref() + .map(|s| s.get_lines()) + .unwrap_or_else(|| vec!["PTY unavailable".to_string()]); + + v_flex() + .size_full() + .bg(rgb(0x0e0f17)) + .p_2() + .child( + // 终端顶部控制栏 + h_flex() + .h(px(28.0)) + .w_full() + .justify_between() + .items_center() + .border_b_1() + .border_color(rgb(0x23263b)) + .pb_1() + .child( + h_flex() + .items_center() + .gap_2() + .text_xs() + .text_color(rgb(0x8a90a2)) + .child("Linux PTY Shell") + .child(format!("({})", self.working_dir)), + ) + .child( + Button::new("clear-term") + .small() + .ghost() + .label("Clear") + .on_click(cx.listener(|this, _event, _window, cx| { + this.clear(cx); + })), + ), + ) + .child( + // 终端输出行展示区 + div() + .flex_1() + .w_full() + .overflow_y_scrollbar() + .p_2() + .text_xs() + .text_color(rgb(0xd1d5db)) + .font_family("monospace") + .children(lines.into_iter().enumerate().map(|(idx, line)| { + div() + .id(idx) + .child(if line.is_empty() { " ".to_string() } else { line }) + })), + ) + .child( + // 命令行输入条 + h_flex() + .h(px(32.0)) + .w_full() + .bg(rgb(0x141522)) + .border_t_1() + .border_color(rgb(0x23263b)) + .items_center() + .px_2() + .gap_2() + .child( + div() + .text_xs() + .text_color(rgb(0x10b981)) + .font_family("monospace") + .child("$"), + ) + .child( + div() + .flex_1() + .text_xs() + .text_color(rgb(0xffffff)) + .font_family("monospace") + .child(if self.current_input.is_empty() { + "Type command here...".to_string() + } else { + self.current_input.clone() + }), + ) + .child( + Button::new("run-cmd") + .small() + .primary() + .label("Execute") + .on_click(cx.listener(|this, _event, _window, cx| { + let cmd = this.current_input.clone(); + if !cmd.is_empty() { + this.send_command(&cmd, cx); + } + })), + ), + ) + } +} diff --git a/linux/src/workbench/toolbar.rs b/linux/src/workbench/toolbar.rs new file mode 100644 index 000000000..20b83d076 --- /dev/null +++ b/linux/src/workbench/toolbar.rs @@ -0,0 +1,126 @@ +use gpui_kit::component::button::{Button, ButtonVariants as _}; +use gpui_kit::component::{h_flex, Sizable as _}; +use gpui_kit::{ + div, px, rgb, Context, EventEmitter, IntoElement, ParentElement as _, + Render, Styled as _, Window, +}; + +#[derive(Debug, Clone)] +pub enum ToolbarEvent { + Save, + Run, + ToggleTerminal, +} + +pub struct ToolbarView { + pub workspace_name: String, + pub git_branch: Option, +} + +impl EventEmitter for ToolbarView {} + +impl ToolbarView { + pub fn new(workspace_root: &str) -> Self { + let name = workspace_root + .rsplit_once('/') + .map(|(_, n)| n.to_string()) + .unwrap_or_else(|| workspace_root.to_string()); + + Self { + workspace_name: name, + git_branch: None, + } + } + + #[allow(dead_code)] + pub fn set_git_branch(&mut self, branch: Option, cx: &mut Context) { + self.git_branch = branch; + cx.notify(); + } +} + +impl Render for ToolbarView { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let branch = self.git_branch.as_deref().unwrap_or("main"); + + h_flex() + .h(px(40.0)) + .w_full() + .bg(rgb(0x10111a)) + .border_b_1() + .border_color(rgb(0x23263b)) + .items_center() + .justify_between() + .px_3() + .child( + // 左侧:项目名称与分支信息 + h_flex() + .items_center() + .gap_3() + .child( + div() + .text_sm() + .text_color(rgb(0xffffff)) + .child(format!("⚡ Lithe | {}", self.workspace_name)), + ) + .child( + div() + .px_2() + .py(px(2.0)) + .rounded_sm() + .bg(rgb(0x1e2030)) + .text_xs() + .text_color(rgb(0x10b981)) + .child(format!("⎇ {}", branch)), + ), + ) + .child( + // 中间:运行与调试配置 + h_flex() + .items_center() + .gap_2() + .child( + Button::new("tb-run") + .small() + .primary() + .label("▶ Run") + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(ToolbarEvent::Run); + })), + ) + .child( + Button::new("tb-debug") + .small() + .ghost() + .label("🐞 Debug") + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(ToolbarEvent::Run); + })), + ), + ) + .child( + // 右侧:保存与面板切换 + h_flex() + .items_center() + .gap_2() + .child( + Button::new("tb-save") + .small() + .ghost() + .label("💾 Save") + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(ToolbarEvent::Save); + })), + ) + .child( + Button::new("tb-terminal") + .small() + .ghost() + .label("⌨ Terminal") + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(ToolbarEvent::ToggleTerminal); + })), + ), + ) + } +} diff --git a/linux/src/workbench/view.rs b/linux/src/workbench/view.rs new file mode 100644 index 000000000..3170f8666 --- /dev/null +++ b/linux/src/workbench/view.rs @@ -0,0 +1,162 @@ +use gpui_kit::component::{h_flex, v_flex, ActiveTheme as _}; +use gpui_kit::{ + div, px, rgb, AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, + Styled as _, Subscription, Window, +}; + +use crate::core::CoreClient; +use crate::workbench::bottom_panel::BottomPanelView; +use crate::workbench::editor::EditorView; +use crate::workbench::sidebar::{SidebarEvent, SidebarView}; +use crate::workbench::toolbar::{ToolbarEvent, ToolbarView}; + +/// Linux 前端主工作台视图组件 +pub struct WorkbenchView { + /// 项目工作区根路径 + pub workspace_root: String, + /// 顶部工具栏 + pub toolbar: Entity, + /// 侧边栏视图实体 + pub sidebar: Entity, + /// 编辑器视图实体 + pub editor: Entity, + /// 底部面板视图实体 + pub bottom_panel: Entity, + client: CoreClient, + _subscriptions: Vec, +} + +impl WorkbenchView { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let workspace_root = std::env::current_dir() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + Self::with_root(workspace_root, window, cx) + } + + pub fn with_root( + workspace_root: String, + _window: &mut Window, + cx: &mut Context, + ) -> Self { + let root_for_toolbar = workspace_root.clone(); + let root_for_sidebar = workspace_root.clone(); + let root_for_editor = workspace_root.clone(); + let root_for_bottom = workspace_root.clone(); + + let toolbar = cx.new(|_cx| ToolbarView::new(&root_for_toolbar)); + let sidebar = cx.new(|cx| SidebarView::new(root_for_sidebar, cx)); + let editor = cx.new(|cx| EditorView::new(root_for_editor, cx)); + let bottom_panel = cx.new(|cx| BottomPanelView::new(root_for_bottom, cx)); + + // 1. 订阅侧边栏事件:点击文件打开 + let sub_sidebar = cx.subscribe(&sidebar, |this, _sidebar, event: &SidebarEvent, cx| { + match event { + SidebarEvent::OpenFile(path) => { + this.open_file(path, cx); + } + } + }); + + // 2. 订阅工具栏事件:保存、运行、终端切换 + let sub_toolbar = cx.subscribe(&toolbar, |this, _toolbar, event: &ToolbarEvent, cx| { + match event { + ToolbarEvent::Save => { + let _ = this.editor.update(cx, |ed, cx| { + ed.save_active(cx); + }); + } + ToolbarEvent::ToggleTerminal => { + let _ = this.bottom_panel.update(cx, |bp, cx| { + bp.toggle_collapsed(cx); + }); + } + ToolbarEvent::Run => { + let _ = this.bottom_panel.update(cx, |bp, cx| { + bp.append_log("[Run] Executing run configuration...".to_string(), cx); + let _ = bp.terminal.update(cx, |term, cx| { + term.send_command("echo '[Lithe Run]' && ls -la", cx); + }); + }); + } + } + }); + + Self { + workspace_root, + toolbar, + sidebar, + editor, + bottom_panel, + client: CoreClient::new(), + _subscriptions: vec![sub_sidebar, sub_toolbar], + } + } + + /// 打开指定文件:对接 `lithe-core` 的 `read_file` 并更新编辑器 + pub fn open_file(&mut self, relative_path: &str, cx: &mut Context) { + let root = self.workspace_root.clone(); + let path = relative_path.to_string(); + let editor = self.editor.clone(); + let client = self.client.clone(); + + cx.spawn(async move |_this, cx| { + let task = client.read_file(&cx, &root, &path); + + match task.await { + Ok(text) => { + let _ = editor.update(cx, |ed, cx| { + ed.open_file(path, text, cx); + }); + } + Err(err) => { + let err_msg = format!("// 读取文件失败: {}\n// 错误信息: {}", path, err); + let _ = editor.update(cx, |ed, cx| { + ed.open_file(path, err_msg, cx); + }); + } + } + }) + .detach(); + } +} + +impl Render for WorkbenchView { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + v_flex() + .size_full() + .bg(cx.theme().background) + .child( + // 1. 顶部工具栏 + self.toolbar.clone(), + ) + .child( + // 2. 主体区域(左右布局) + h_flex() + .flex_1() + .w_full() + .child( + // 左侧分栏:侧边栏(Explorer / Search / Git) + div() + .w(px(280.0)) + .h_full() + .flex_shrink_0() + .border_r_1() + .border_color(rgb(0x23263b)) + .child(self.sidebar.clone()), + ) + .child( + // 右侧分栏:多标签代码编辑器 + div() + .flex_1() + .h_full() + .child(self.editor.clone()), + ), + ) + .child( + // 3. 底部面板(终端 / 日志 / 问题) + self.bottom_panel.clone(), + ) + } +} diff --git a/scripts/build-linux.sh b/scripts/build-linux.sh new file mode 100755 index 000000000..9457d86b7 --- /dev/null +++ b/scripts/build-linux.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" + +PROFILE="debug" +TARGET="" +EXTRA_ARGS=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --release) + PROFILE="release" + shift + ;; + --debug) + PROFILE="debug" + shift + ;; + --target) + TARGET="$2" + shift 2 + ;; + *) + EXTRA_ARGS+=("$1") + shift + ;; + esac +done + +cd "$ROOT_DIR" + +CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$ROOT_DIR/target}" +export CARGO_TARGET_DIR + +BUILD_ARGS=(build --manifest-path "$ROOT_DIR/linux/Cargo.toml" -p lithe-linux) + +if [[ -n "$TARGET" ]]; then + BUILD_ARGS+=(--target "$TARGET") +fi + +if [[ "$PROFILE" == "release" ]]; then + BUILD_ARGS+=(--release) +fi + +if [[ ${#EXTRA_ARGS[@]} -gt 0 ]]; then + BUILD_ARGS+=("${EXTRA_ARGS[@]}") +fi + +printf '==> Building Lithe Linux frontend (%s)...\n' "$PROFILE" +cargo "${BUILD_ARGS[@]}" + +OUTPUT_BIN="$CARGO_TARGET_DIR" +if [[ -n "$TARGET" ]]; then + OUTPUT_BIN="$OUTPUT_BIN/$TARGET" +fi +OUTPUT_BIN="$OUTPUT_BIN/$PROFILE/lithe-linux" + +if [[ -f "$OUTPUT_BIN" ]]; then + printf '==> Build successful: %s\n' "$OUTPUT_BIN" +else + printf '==> Error: Output binary not found at %s\n' "$OUTPUT_BIN" >&2 + exit 1 +fi diff --git a/scripts/package-linux.sh b/scripts/package-linux.sh new file mode 100755 index 000000000..ae0e6d1cd --- /dev/null +++ b/scripts/package-linux.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" + +PROFILE="release" +VERSION="${LITHE_VERSION:-0.1.0}" +ARCH="${LITHE_ARCH:-$(uname -m)}" +DIST_DIR="${LITHE_DIST_DIR:-$ROOT_DIR/dist}" + +while [[ $# -gt 0 ]]; do + case "$1" in + --debug) + PROFILE="debug" + shift + ;; + --release) + PROFILE="release" + shift + ;; + --version) + VERSION="$2" + shift 2 + ;; + *) + shift + ;; + esac +done + +cd "$ROOT_DIR" + +# 1. Build lithe-linux binary +"$SCRIPT_DIR/build-linux.sh" "--$PROFILE" + +TARGET_BIN="${CARGO_TARGET_DIR:-$ROOT_DIR/target}/$PROFILE/lithe-linux" +PACKAGE_NAME="lithe-linux-${ARCH}-${VERSION}" +STAGE_DIR="$DIST_DIR/$PACKAGE_NAME" + +rm -rf "$STAGE_DIR" +mkdir -p "$STAGE_DIR/bin" +mkdir -p "$STAGE_DIR/share/applications" +mkdir -p "$STAGE_DIR/share/icons/hicolor/512x512/apps" + +# 2. Copy binary +cp "$TARGET_BIN" "$STAGE_DIR/bin/lithe" +chmod +x "$STAGE_DIR/bin/lithe" + +# 3. Create Desktop Entry +cat < "$STAGE_DIR/share/applications/lithe.desktop" +[Desktop Entry] +Name=Lithe +Comment=Lithe IDE for Linux +Exec=lithe %F +Icon=lithe +Terminal=false +Type=Application +Categories=Development;IDE; +StartupWMClass=lithe +EOF + +# 4. Copy icon if available +ICON_SRC="$ROOT_DIR/assets/icon.png" +if [[ ! -f "$ICON_SRC" ]]; then + ICON_SRC=$(find "$ROOT_DIR/windows/tauri/src/extensions" -name "linux.svg" | head -n 1 || true) +fi + +if [[ -f "$ICON_SRC" ]]; then + cp "$ICON_SRC" "$STAGE_DIR/share/icons/hicolor/512x512/apps/lithe.png" || true +fi + +# 5. Create tar.gz archive +mkdir -p "$DIST_DIR" +TARBALL="$DIST_DIR/${PACKAGE_NAME}.tar.gz" +tar -czf "$TARBALL" -C "$DIST_DIR" "$PACKAGE_NAME" + +printf '==> Linux package created: %s\n' "$TARBALL" From 0bb39ab5710d1945b1af1f430307d2b9ea8834dc Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 00:00:17 +0800 Subject: [PATCH 02/59] =?UTF-8?q?feat(linux):=20=E5=AF=B9=E9=BD=90=20Tauri?= =?UTF-8?q?/IntelliJ=20=E6=9E=B6=E6=9E=84=E8=AE=BE=E8=AE=A1=E4=B8=8E?= =?UTF-8?q?=E6=9A=97=E8=89=B2=E4=B8=BB=E9=A2=98=20Tab=20=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ThemeColors 集中定义深色主题调色板,并在应用入口强制开启 ThemeMode::Dark - 重构标题栏 (ToolbarView):加入左上角完整桌面菜单栏 (File/Edit/View/Run/Terminal/Help)、项目与分支选择徽章、Search Everywhere 快捷入口 - 新增垂直活动栏 (ActivityRailView):支持在 Files、Git、Search 与 Terminal 间切换/展开侧边栏 - 重构多标签代码区 (EditorView):新增面包屑路径导航条与代码行号槽 - 重写底栏 (BottomPanelView) 与侧边栏 (SidebarView) Tab 控件,修复暗色下按钮低对比度不可见问题 - 新增全局底部状态栏 (StatusBarView):统一展示分支、光标位置、语言及编码状态 --- linux/src/lib.rs | 1 + linux/src/main.rs | 4 +- linux/src/theme.rs | 85 +++++++ linux/src/workbench/activity_rail.rs | 147 ++++++++++++ linux/src/workbench/bottom_panel.rs | 119 +++++----- linux/src/workbench/editor.rs | 103 ++++----- linux/src/workbench/mod.rs | 6 + linux/src/workbench/sidebar.rs | 125 ++++++----- linux/src/workbench/status_bar.rs | 110 +++++++++ linux/src/workbench/toolbar.rs | 321 ++++++++++++++++++++++++--- linux/src/workbench/view.rs | 202 ++++++++++++++--- 11 files changed, 992 insertions(+), 231 deletions(-) create mode 100644 linux/src/theme.rs create mode 100644 linux/src/workbench/activity_rail.rs create mode 100644 linux/src/workbench/status_bar.rs diff --git a/linux/src/lib.rs b/linux/src/lib.rs index 7c4fe1ee7..f8a5e12e9 100644 --- a/linux/src/lib.rs +++ b/linux/src/lib.rs @@ -1,4 +1,5 @@ pub mod core; +pub mod theme; pub mod workbench; pub use workbench::WorkbenchView; diff --git a/linux/src/main.rs b/linux/src/main.rs index 69c77ca8e..e614fba38 100644 --- a/linux/src/main.rs +++ b/linux/src/main.rs @@ -1,7 +1,8 @@ mod core; +mod theme; mod workbench; -use gpui_kit::component::Root; +use gpui_kit::component::{Root, Theme, ThemeMode}; use gpui_kit::*; use workbench::WorkbenchView; @@ -10,6 +11,7 @@ fn main() { app.run(move |cx| { gpui_kit::init(cx); + Theme::change(ThemeMode::Dark, None, cx); cx.open_window(WindowOptions::default(), |window, cx| { let view = cx.new(|cx| WorkbenchView::new(window, cx)); diff --git a/linux/src/theme.rs b/linux/src/theme.rs new file mode 100644 index 000000000..6b9bacdf0 --- /dev/null +++ b/linux/src/theme.rs @@ -0,0 +1,85 @@ +use gpui_kit::Rgba; + +pub const fn const_rgb(hex: u32) -> 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: 1.0 } +} + +pub struct ThemeColors; + +#[allow(dead_code)] +impl ThemeColors { + pub const BG_TITLEBAR: Rgba = const_rgb(0x1e1f22); + pub const BG_ACTIVITY_RAIL: Rgba = const_rgb(0x18191c); + pub const BG_SIDEBAR: Rgba = const_rgb(0x1e1f22); + pub const BG_EDITOR: Rgba = const_rgb(0x2b2d30); + pub const BG_TAB_BAR: Rgba = const_rgb(0x1e1f22); + pub const BG_TAB_ACTIVE: Rgba = const_rgb(0x2b2d30); + pub const BG_TAB_HOVER: Rgba = const_rgb(0x24262b); + pub const BG_BOTTOM_PANEL: Rgba = const_rgb(0x1e1f22); + pub const BG_STATUSBAR: Rgba = const_rgb(0x18191c); + pub const BORDER: Rgba = const_rgb(0x393b40); + pub const ACCENT_BLUE: Rgba = const_rgb(0x3574f0); + pub const ACCENT_GREEN: Rgba = const_rgb(0x39a36b); + pub const TEXT_PRIMARY: Rgba = const_rgb(0xdfdfe0); + pub const TEXT_MUTED: Rgba = const_rgb(0x818594); + + #[inline] + pub fn bg_titlebar() -> Rgba { + Self::BG_TITLEBAR + } + #[inline] + pub fn bg_activity_rail() -> Rgba { + Self::BG_ACTIVITY_RAIL + } + #[inline] + pub fn bg_sidebar() -> Rgba { + Self::BG_SIDEBAR + } + #[inline] + pub fn bg_editor() -> Rgba { + Self::BG_EDITOR + } + #[inline] + pub fn bg_tab_bar() -> Rgba { + Self::BG_TAB_BAR + } + #[inline] + pub fn bg_tab_active() -> Rgba { + Self::BG_TAB_ACTIVE + } + #[inline] + pub fn bg_tab_hover() -> Rgba { + Self::BG_TAB_HOVER + } + #[inline] + pub fn bg_bottom_panel() -> Rgba { + Self::BG_BOTTOM_PANEL + } + #[inline] + pub fn bg_statusbar() -> Rgba { + Self::BG_STATUSBAR + } + #[inline] + pub fn border() -> Rgba { + Self::BORDER + } + #[inline] + pub fn accent_blue() -> Rgba { + Self::ACCENT_BLUE + } + #[inline] + pub fn accent_green() -> Rgba { + Self::ACCENT_GREEN + } + #[inline] + pub fn text_primary() -> Rgba { + Self::TEXT_PRIMARY + } + #[inline] + pub fn text_muted() -> Rgba { + Self::TEXT_MUTED + } +} diff --git a/linux/src/workbench/activity_rail.rs b/linux/src/workbench/activity_rail.rs new file mode 100644 index 000000000..a8279f5d9 --- /dev/null +++ b/linux/src/workbench/activity_rail.rs @@ -0,0 +1,147 @@ +use gpui_kit::component::v_flex; +use gpui_kit::prelude::FluentBuilder as _; +use gpui_kit::{ + div, px, Context, EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _, + Render, StatefulInteractiveElement as _, Styled as _, Window, +}; + +use crate::theme::ThemeColors; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ActivityTab { + Explorer, + Git, + Search, +} + +#[derive(Debug, Clone)] +pub enum ActivityRailEvent { + SelectTab(ActivityTab), + ToggleTerminal, + OpenSettings, +} + +pub struct ActivityRailView { + pub active_tab: Option, +} + +impl EventEmitter for ActivityRailView {} + +impl ActivityRailView { + pub fn new() -> Self { + Self { + active_tab: Some(ActivityTab::Explorer), + } + } + + pub fn set_active_tab(&mut self, tab: Option, cx: &mut Context) { + self.active_tab = tab; + cx.notify(); + } +} + +impl Render for ActivityRailView { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let active = self.active_tab; + + v_flex() + .w(px(42.0)) + .h_full() + .bg(ThemeColors::bg_activity_rail()) + .border_r_1() + .border_color(ThemeColors::border()) + .items_center() + .justify_between() + .py_2() + .child( + // 顶部工具窗口图标列表 + v_flex() + .w_full() + .items_center() + .gap_1() + .child(self.render_rail_button( + "rail-files", + "📁", + "Files (Project Explorer)", + active == Some(ActivityTab::Explorer), + ActivityRailEvent::SelectTab(ActivityTab::Explorer), + cx, + )) + .child(self.render_rail_button( + "rail-git", + "⎇", + "Version Control (Git)", + active == Some(ActivityTab::Git), + ActivityRailEvent::SelectTab(ActivityTab::Git), + cx, + )) + .child(self.render_rail_button( + "rail-search", + "🔍", + "Search Everywhere", + active == Some(ActivityTab::Search), + ActivityRailEvent::SelectTab(ActivityTab::Search), + cx, + )) + .child(self.render_rail_button( + "rail-terminal", + "⌨", + "Terminal", + false, + ActivityRailEvent::ToggleTerminal, + cx, + )), + ) + .child( + // 底部设置图标 + v_flex() + .w_full() + .items_center() + .child(self.render_rail_button( + "rail-settings", + "⚙", + "Settings", + false, + ActivityRailEvent::OpenSettings, + cx, + )), + ) + } +} + +impl ActivityRailView { + fn render_rail_button( + &self, + id: &'static str, + icon: &'static str, + _tooltip: &'static str, + is_active: bool, + event: ActivityRailEvent, + cx: &mut Context, + ) -> impl IntoElement { + div() + .id(id) + .w(px(34.0)) + .h(px(34.0)) + .rounded_md() + .flex() + .items_center() + .justify_center() + .cursor_pointer() + .text_sm() + .when(is_active, |btn| { + btn.bg(ThemeColors::bg_sidebar()) + .text_color(ThemeColors::accent_blue()) + .border_l_2() + .border_color(ThemeColors::accent_blue()) + }) + .when(!is_active, |btn| { + btn.text_color(ThemeColors::text_muted()) + .hover(|h| h.bg(ThemeColors::bg_tab_hover()).text_color(ThemeColors::text_primary())) + }) + .child(icon) + .on_click(cx.listener(move |_this, _event, _window, cx| { + cx.emit(event.clone()); + })) + } +} diff --git a/linux/src/workbench/bottom_panel.rs b/linux/src/workbench/bottom_panel.rs index 4a5406f2a..8fea5f41a 100644 --- a/linux/src/workbench/bottom_panel.rs +++ b/linux/src/workbench/bottom_panel.rs @@ -3,10 +3,11 @@ use gpui_kit::component::scroll::ScrollableElement as _; use gpui_kit::component::{h_flex, v_flex, Sizable as _}; use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::{ - div, px, rgb, AppContext as _, Context, Entity, IntoElement, - ParentElement as _, Render, Styled as _, Window, + div, px, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement, + ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Window, }; +use crate::theme::ThemeColors; use crate::workbench::terminal::TerminalView; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -52,17 +53,48 @@ impl BottomPanelView { self.output_logs.push(log); cx.notify(); } + + fn render_tab_button( + &self, + id: &'static str, + label: String, + is_active: bool, + tab: BottomTab, + cx: &mut Context, + ) -> impl IntoElement { + div() + .id(id) + .px_3() + .py_1() + .rounded_t_sm() + .cursor_pointer() + .text_xs() + .when(is_active, |btn| { + btn.bg(ThemeColors::bg_bottom_panel()) + .text_color(ThemeColors::text_primary()) + .border_b_2() + .border_color(ThemeColors::accent_blue()) + }) + .when(!is_active, |btn| { + btn.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.set_tab(tab, cx); + })) + } } impl Render for BottomPanelView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { if self.is_collapsed { return div() - .h(px(28.0)) + .h(px(26.0)) .w_full() - .bg(rgb(0x141522)) + .bg(ThemeColors::BG_STATUSBAR) .border_t_1() - .border_color(rgb(0x23263b)) + .border_color(ThemeColors::BORDER) .flex() .items_center() .justify_between() @@ -70,7 +102,7 @@ impl Render for BottomPanelView { .child( div() .text_xs() - .text_color(rgb(0x8a90a2)) + .text_color(ThemeColors::TEXT_MUTED) .child("Panel collapsed"), ) .child( @@ -87,17 +119,17 @@ impl Render for BottomPanelView { v_flex() .h(px(240.0)) .w_full() - .bg(rgb(0x11121d)) + .bg(ThemeColors::BG_BOTTOM_PANEL) .border_t_1() - .border_color(rgb(0x23263b)) + .border_color(ThemeColors::BORDER) .child( // 顶部 Tab 切换栏 h_flex() .h(px(32.0)) .w_full() - .bg(rgb(0x141522)) + .bg(ThemeColors::BG_TAB_BAR) .border_b_1() - .border_color(rgb(0x23263b)) + .border_color(ThemeColors::BORDER) .items_center() .justify_between() .px_2() @@ -105,48 +137,27 @@ impl Render for BottomPanelView { h_flex() .items_center() .gap_1() - .child( - Button::new("tab-terminal") - .small() - .when(self.active_tab == BottomTab::Terminal, |btn| { - btn.primary() - }) - .when(self.active_tab != BottomTab::Terminal, |btn| { - btn.ghost() - }) - .label("Terminal") - .on_click(cx.listener(|this, _event, _window, cx| { - this.set_tab(BottomTab::Terminal, cx); - })), - ) - .child( - Button::new("tab-output") - .small() - .when(self.active_tab == BottomTab::Output, |btn| { - btn.primary() - }) - .when(self.active_tab != BottomTab::Output, |btn| { - btn.ghost() - }) - .label("Output") - .on_click(cx.listener(|this, _event, _window, cx| { - this.set_tab(BottomTab::Output, cx); - })), - ) - .child( - Button::new("tab-problems") - .small() - .when(self.active_tab == BottomTab::Problems, |btn| { - btn.primary() - }) - .when(self.active_tab != BottomTab::Problems, |btn| { - btn.ghost() - }) - .label(format!("Problems ({})", self.problems.len())) - .on_click(cx.listener(|this, _event, _window, cx| { - this.set_tab(BottomTab::Problems, cx); - })), - ), + .child(self.render_tab_button( + "tab-terminal", + "Terminal".to_string(), + self.active_tab == BottomTab::Terminal, + BottomTab::Terminal, + cx, + )) + .child(self.render_tab_button( + "tab-output", + "Output".to_string(), + self.active_tab == BottomTab::Output, + BottomTab::Output, + cx, + )) + .child(self.render_tab_button( + "tab-problems", + format!("Problems ({})", self.problems.len()), + self.active_tab == BottomTab::Problems, + BottomTab::Problems, + cx, + )), ) .child( Button::new("collapse-panel") @@ -171,7 +182,7 @@ impl Render for BottomPanelView { .overflow_y_scrollbar() .text_xs() .font_family("monospace") - .text_color(rgb(0x9ca3af)) + .text_color(ThemeColors::text_muted()) .children( self.output_logs .iter() @@ -184,7 +195,7 @@ impl Render for BottomPanelView { .p_3() .overflow_y_scrollbar() .text_xs() - .text_color(rgb(0x9ca3af)) + .text_color(ThemeColors::text_muted()) .child(if self.problems.is_empty() { "No problems have been detected in the workspace.".to_string() } else { diff --git a/linux/src/workbench/editor.rs b/linux/src/workbench/editor.rs index 09f450427..611850fff 100644 --- a/linux/src/workbench/editor.rs +++ b/linux/src/workbench/editor.rs @@ -7,6 +7,7 @@ use gpui_kit::{ }; use crate::core::CoreClient; +use crate::theme::ThemeColors; #[derive(Debug, Clone)] pub struct EditorTab { @@ -14,11 +15,13 @@ pub struct EditorTab { pub title: String, pub content: String, pub is_dirty: bool, + #[allow(dead_code)] pub cursor_line: usize, + #[allow(dead_code)] pub cursor_col: usize, } -/// 多标签代码编辑器组件 +/// 多标签代码编辑器组件(对齐 IntelliJ / Tauri 视觉规范) pub struct EditorView { pub workspace_root: String, pub tabs: Vec, @@ -148,15 +151,15 @@ impl Render for EditorView { v_flex() .size_full() - .bg(rgb(0x13141f)) + .bg(ThemeColors::BG_EDITOR) .child( // 1. 顶部标签栏(Tab Bar) h_flex() - .h(px(36.0)) + .h(px(34.0)) .w_full() - .bg(rgb(0x10111a)) + .bg(ThemeColors::BG_TAB_BAR) .border_b_1() - .border_color(rgb(0x23263b)) + .border_color(ThemeColors::BORDER) .items_center() .overflow_x_scrollbar() .px_1() @@ -167,17 +170,19 @@ impl Render for EditorView { h_flex() .id(idx) - .h(px(32.0)) + .h(px(34.0)) .items_center() .gap_2() .px_3() - .rounded_t_sm() .cursor_pointer() .when(is_active, |t| { - t.bg(rgb(0x181926)).border_t_2().border_color(rgb(0x3b82f6)) + t.bg(ThemeColors::BG_TAB_ACTIVE) + .border_t_2() + .border_color(ThemeColors::ACCENT_BLUE) }) .when(!is_active, |t| { - t.bg(rgb(0x10111a)).hover(|h| h.bg(rgb(0x151624))) + 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); @@ -187,9 +192,9 @@ impl Render for EditorView { div() .text_xs() .text_color(if is_active { - rgb(0xffffff) + ThemeColors::TEXT_PRIMARY } else { - rgb(0x9ca3af) + ThemeColors::TEXT_MUTED }) .child(title), ) @@ -197,7 +202,7 @@ impl Render for EditorView { t.child( div() .text_xs() - .text_color(rgb(0x3b82f6)) + .text_color(ThemeColors::ACCENT_BLUE) .child("●"), ) }) @@ -205,7 +210,7 @@ impl Render for EditorView { div() .id(("close-tab", idx)) .text_xs() - .text_color(rgb(0x6b7280)) + .text_color(ThemeColors::TEXT_MUTED) .hover(|h| h.text_color(rgb(0xef4444))) .child("×") .on_click(cx.listener(move |this, _event, _window, cx| { @@ -214,8 +219,31 @@ impl Render for EditorView { ) })), ) + .when_some(active_tab.as_ref(), |this, tab| { + // 2. 面包屑导航栏(Breadcrumb Bar) + 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_1() + .text_xs() + .text_color(ThemeColors::TEXT_MUTED) + .child("📂") + .child( + tab.path + .split('/') + .collect::>() + .join(" › "), + ), + ) + }) .child( - // 2. 中央内容编辑区 + // 3. 中央代码内容区 div().flex_1().w_full().overflow_scrollbar().child( if let Some(tab) = &active_tab { let lines: Vec = tab.content.lines().map(|s| s.to_string()).collect(); @@ -227,19 +255,19 @@ impl Render for EditorView { .font_family("monospace") .text_xs() .child( - // 行号列 + // 代码行号槽(Gutter) v_flex() .flex_shrink_0() - .text_color(rgb(0x4b5563)) + .text_color(ThemeColors::TEXT_MUTED) .children((1..=lines.len().max(1)).map(|num| { div().h(px(20.0)).child(format!("{num:4}")) })), ) .child( - // 代码文本列 + // 代码文本展示行 v_flex() .flex_1() - .text_color(rgb(0xe5e7eb)) + .text_color(ThemeColors::TEXT_PRIMARY) .children(lines.into_iter().enumerate().map(|(idx, line)| { div() .id(idx) @@ -248,49 +276,16 @@ impl Render for EditorView { })), ) } else { - // 空状态 + // 无打开文件欢迎占位 h_flex() .size_full() .items_center() .justify_center() .text_sm() - .text_color(rgb(0x6b7280)) - .child("No file opened. Select a file from the sidebar.") + .text_color(ThemeColors::TEXT_MUTED) + .child("No file opened. Select a file from the project explorer or press Ctrl+P.") }, ), ) - .child( - // 3. 底部状态栏 - h_flex() - .h(px(24.0)) - .w_full() - .bg(rgb(0x10111a)) - .border_t_1() - .border_color(rgb(0x23263b)) - .items_center() - .justify_between() - .px_3() - .text_xs() - .text_color(rgb(0x6b7280)) - .child( - h_flex().items_center().gap_3().child(if let Some(tab) = &active_tab { - format!("Ln {}, Col {}", tab.cursor_line, tab.cursor_col) - } else { - "Ready".to_string() - }), - ) - .child( - h_flex() - .items_center() - .gap_3() - .child(if let Some(tab) = &active_tab { - Self::language_name(&tab.path) - } else { - "Plain Text" - }) - .child("UTF-8") - .child("Lithe Core"), - ), - ) } } diff --git a/linux/src/workbench/mod.rs b/linux/src/workbench/mod.rs index a2031fb4c..5a3e7857f 100644 --- a/linux/src/workbench/mod.rs +++ b/linux/src/workbench/mod.rs @@ -1,10 +1,14 @@ +pub mod activity_rail; pub mod bottom_panel; pub mod editor; pub mod sidebar; +pub mod status_bar; pub mod terminal; pub mod toolbar; pub mod view; +#[allow(unused_imports)] +pub use activity_rail::{ActivityRailEvent, ActivityRailView, ActivityTab}; #[allow(unused_imports)] pub use bottom_panel::BottomPanelView; #[allow(unused_imports)] @@ -12,6 +16,8 @@ pub use editor::EditorView; #[allow(unused_imports)] pub use sidebar::SidebarView; #[allow(unused_imports)] +pub use status_bar::StatusBarView; +#[allow(unused_imports)] pub use terminal::TerminalView; #[allow(unused_imports)] pub use toolbar::ToolbarView; diff --git a/linux/src/workbench/sidebar.rs b/linux/src/workbench/sidebar.rs index 66c7af64e..a2eb22e46 100644 --- a/linux/src/workbench/sidebar.rs +++ b/linux/src/workbench/sidebar.rs @@ -8,6 +8,7 @@ use gpui_kit::{ use serde::{Deserialize, Serialize}; use crate::core::CoreClient; +use crate::theme::ThemeColors; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SidebarTab { @@ -326,23 +327,54 @@ impl SidebarView { } false } + + fn render_tab_button( + &self, + id: &'static str, + label: String, + is_active: bool, + tab: SidebarTab, + cx: &mut Context, + ) -> impl IntoElement { + div() + .id(id) + .px_3() + .py_1() + .rounded_t_sm() + .cursor_pointer() + .text_xs() + .when(is_active, |btn| { + btn.bg(ThemeColors::bg_sidebar()) + .text_color(ThemeColors::text_primary()) + .border_b_2() + .border_color(ThemeColors::accent_blue()) + }) + .when(!is_active, |btn| { + btn.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.set_tab(tab, cx); + })) + } } impl Render for SidebarView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() - .bg(rgb(0x161724)) + .bg(ThemeColors::BG_SIDEBAR) .border_r_1() - .border_color(rgb(0x23263b)) + .border_color(ThemeColors::BORDER) .child( // 顶部视图切换栏 (Explorer / Search / Git) h_flex() .h(px(36.0)) .w_full() - .bg(rgb(0x141522)) + .bg(ThemeColors::BG_TAB_BAR) .border_b_1() - .border_color(rgb(0x23263b)) + .border_color(ThemeColors::BORDER) .items_center() .justify_between() .px_2() @@ -350,44 +382,27 @@ impl Render for SidebarView { h_flex() .items_center() .gap_1() - .child( - Button::new("tab-explorer") - .small() - .when(self.active_tab == SidebarTab::Explorer, |b| { - b.primary() - }) - .when(self.active_tab != SidebarTab::Explorer, |b| { - b.ghost() - }) - .label("Files") - .on_click(cx.listener(|this, _event, _window, cx| { - this.set_tab(SidebarTab::Explorer, cx); - })), - ) - .child( - Button::new("tab-search") - .small() - .when(self.active_tab == SidebarTab::Search, |b| { - b.primary() - }) - .when(self.active_tab != SidebarTab::Search, |b| { - b.ghost() - }) - .label("Search") - .on_click(cx.listener(|this, _event, _window, cx| { - this.set_tab(SidebarTab::Search, cx); - })), - ) - .child( - Button::new("tab-git") - .small() - .when(self.active_tab == SidebarTab::Git, |b| b.primary()) - .when(self.active_tab != SidebarTab::Git, |b| b.ghost()) - .label(format!("Git ({})", self.git_changes.len())) - .on_click(cx.listener(|this, _event, _window, cx| { - this.set_tab(SidebarTab::Git, cx); - })), - ), + .child(self.render_tab_button( + "tab-explorer", + "Files".to_string(), + self.active_tab == SidebarTab::Explorer, + SidebarTab::Explorer, + cx, + )) + .child(self.render_tab_button( + "tab-search", + "Search".to_string(), + self.active_tab == SidebarTab::Search, + SidebarTab::Search, + cx, + )) + .child(self.render_tab_button( + "tab-git", + format!("Git ({})", self.git_changes.len()), + self.active_tab == SidebarTab::Git, + SidebarTab::Git, + cx, + )), ) .child( Button::new("refresh-btn") @@ -459,10 +474,10 @@ impl SidebarView { .pr_2() .text_xs() .when(is_selected, |row| { - row.bg(rgb(0x2a2d42)).text_color(rgb(0xffffff)) + row.bg(ThemeColors::BG_TAB_ACTIVE).text_color(ThemeColors::TEXT_PRIMARY) }) .when(!is_selected, |row| { - row.text_color(rgb(0xcfd3e0)).hover(|h| h.bg(rgb(0x1e2030))) + row.text_color(ThemeColors::TEXT_PRIMARY).hover(|h| h.bg(ThemeColors::BG_TAB_HOVER)) }) .child(if is_dir { if item.is_expanded { "📂 " } else { "📁 " } @@ -495,13 +510,15 @@ impl SidebarView { .h(px(28.0)) .w_full() .items_center() - .bg(rgb(0x1e2030)) + .bg(ThemeColors::BG_TAB_ACTIVE) + .border_1() + .border_color(ThemeColors::BORDER) .rounded_sm() .px_2() .child( div() .text_xs() - .text_color(rgb(0x8a90a2)) + .text_color(ThemeColors::TEXT_MUTED) .child("Search files or text..."), ), ) @@ -530,19 +547,19 @@ impl SidebarView { .p_1() .rounded_sm() .cursor_pointer() - .hover(|h| h.bg(rgb(0x1e2030))) + .hover(|h| h.bg(ThemeColors::BG_TAB_HOVER)) .child( h_flex() .items_center() .gap_1() .text_xs() - .text_color(rgb(0x60a5fa)) + .text_color(ThemeColors::ACCENT_BLUE) .child(format!("{}{}", path, line_info)), ) .child( div() .text_xs() - .text_color(rgb(0x9ca3af)) + .text_color(ThemeColors::TEXT_MUTED) .child(preview), ) .on_click(cx.listener({ @@ -570,16 +587,16 @@ impl SidebarView { .gap_2() .pb_2() .border_b_1() - .border_color(rgb(0x23263b)) + .border_color(ThemeColors::BORDER) .text_xs() - .text_color(rgb(0x10b981)) + .text_color(ThemeColors::ACCENT_GREEN) .child("⎇ Branch:") .child(branch), ) .child( div() .text_xs() - .text_color(rgb(0x8a90a2)) + .text_color(ThemeColors::TEXT_MUTED) .child(format!("Changed Files ({})", self.git_changes.len())), ) .children(self.git_changes.iter().enumerate().map(|(idx, change)| { @@ -595,13 +612,13 @@ impl SidebarView { .px_2() .rounded_sm() .cursor_pointer() - .hover(|h| h.bg(rgb(0x1e2030))) + .hover(|h| h.bg(ThemeColors::BG_TAB_HOVER)) .text_xs() .child( h_flex() .items_center() .gap_2() - .text_color(rgb(0xcfd3e0)) + .text_color(ThemeColors::TEXT_PRIMARY) .child(path.clone()), ) .child( diff --git a/linux/src/workbench/status_bar.rs b/linux/src/workbench/status_bar.rs new file mode 100644 index 000000000..66a447972 --- /dev/null +++ b/linux/src/workbench/status_bar.rs @@ -0,0 +1,110 @@ +use gpui_kit::component::h_flex; +use gpui_kit::{ + div, px, Context, IntoElement, ParentElement as _, Render, Styled as _, Window, +}; + +use crate::theme::ThemeColors; + +pub struct StatusBarView { + pub git_branch: Option, + pub current_file: Option, + pub cursor_line: usize, + pub cursor_col: usize, + pub language: String, + pub encoding: String, +} + +impl StatusBarView { + pub fn new() -> Self { + Self { + git_branch: None, + current_file: None, + cursor_line: 1, + cursor_col: 1, + language: "Plain Text".to_string(), + encoding: "UTF-8".to_string(), + } + } + + pub fn set_file_info( + &mut self, + file: Option, + line: usize, + col: usize, + lang: String, + cx: &mut Context, + ) { + self.current_file = file; + self.cursor_line = line; + self.cursor_col = col; + self.language = lang; + cx.notify(); + } + + #[allow(dead_code)] + pub fn set_git_branch(&mut self, branch: Option, cx: &mut Context) { + self.git_branch = branch; + cx.notify(); + } +} + +impl Render for StatusBarView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let branch = self.git_branch.clone().unwrap_or_else(|| "main".to_string()); + let file_label = self + .current_file + .clone() + .unwrap_or_else(|| "No file active".to_string()); + + h_flex() + .h(px(24.0)) + .w_full() + .bg(ThemeColors::bg_statusbar()) + .border_t_1() + .border_color(ThemeColors::border()) + .items_center() + .justify_between() + .px_3() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child( + // 左侧状态栏项目 + h_flex() + .items_center() + .gap_3() + .child( + h_flex() + .items_center() + .gap_1() + .text_color(ThemeColors::accent_green()) + .child("⎇") + .child(branch), + ) + .child(div().text_color(ThemeColors::border()).child("|")) + .child( + div() + .text_color(ThemeColors::text_primary()) + .child(file_label), + ), + ) + .child( + // 右侧状态栏项目 + h_flex() + .items_center() + .gap_3() + .child(format!("Ln {}, Col {}", self.cursor_line, self.cursor_col)) + .child(div().text_color(ThemeColors::border()).child("|")) + .child(self.language.clone()) + .child(div().text_color(ThemeColors::border()).child("|")) + .child(self.encoding.clone()) + .child(div().text_color(ThemeColors::border()).child("|")) + .child("4 spaces") + .child(div().text_color(ThemeColors::border()).child("|")) + .child( + div() + .text_color(ThemeColors::accent_blue()) + .child("Lithe Core"), + ), + ) + } +} diff --git a/linux/src/workbench/toolbar.rs b/linux/src/workbench/toolbar.rs index 20b83d076..748f52b4d 100644 --- a/linux/src/workbench/toolbar.rs +++ b/linux/src/workbench/toolbar.rs @@ -1,15 +1,29 @@ use gpui_kit::component::button::{Button, ButtonVariants as _}; +use gpui_kit::component::menu::{DropdownMenu as _, PopupMenuItem}; use gpui_kit::component::{h_flex, Sizable as _}; use gpui_kit::{ - div, px, rgb, Context, EventEmitter, IntoElement, ParentElement as _, - Render, Styled as _, Window, + div, px, Context, EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _, + Render, StatefulInteractiveElement as _, Styled as _, Window, }; +use crate::theme::ThemeColors; + #[derive(Debug, Clone)] pub enum ToolbarEvent { + NewFile, Save, + CloseTab, Run, + Debug, + #[allow(dead_code)] + Stop, ToggleTerminal, + ClearTerminal, + ToggleSidebar, + RefreshWorkspace, + QuickOpen, + About, + Exit, } pub struct ToolbarView { @@ -41,41 +55,297 @@ impl ToolbarView { impl Render for ToolbarView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let branch = self.git_branch.as_deref().unwrap_or("main"); + let branch = self.git_branch.clone().unwrap_or_else(|| "main".to_string()); + let view = cx.entity(); h_flex() .h(px(40.0)) .w_full() - .bg(rgb(0x10111a)) + .bg(ThemeColors::bg_titlebar()) .border_b_1() - .border_color(rgb(0x23263b)) + .border_color(ThemeColors::border()) .items_center() .justify_between() .px_3() .child( - // 左侧:项目名称与分支信息 + // 1. 左侧:Logo + 菜单栏 + 项目名称选择器 + 分支 h_flex() .items_center() - .gap_3() + .gap_1() .child( - div() - .text_sm() - .text_color(rgb(0xffffff)) - .child(format!("⚡ Lithe | {}", self.workspace_name)), + h_flex() + .items_center() + .gap_1() + .pr_2() + .child( + div() + .text_sm() + .text_color(ThemeColors::accent_blue()) + .child("⚡"), + ) + .child( + div() + .text_sm() + .text_color(ThemeColors::text_primary()) + .child("Lithe"), + ), ) + // File 菜单 + .child({ + let v = view.clone(); + Button::new("menu-file") + .small() + .ghost() + .label("File") + .dropdown_menu(move |menu, _window, _cx| { + let v_new = v.clone(); + let v_save = v.clone(); + let v_close = v.clone(); + let v_exit = v.clone(); + + menu.item( + PopupMenuItem::new("New File (Ctrl+N)") + .on_click(move |_, _, cx| { + v_new.update(cx, |_, cx| cx.emit(ToolbarEvent::NewFile)); + }), + ) + .item( + PopupMenuItem::new("Save (Ctrl+S)") + .on_click(move |_, _, cx| { + v_save.update(cx, |_, cx| cx.emit(ToolbarEvent::Save)); + }), + ) + .item( + PopupMenuItem::new("Close Active Tab (Ctrl+W)") + .on_click(move |_, _, cx| { + v_close.update(cx, |_, cx| cx.emit(ToolbarEvent::CloseTab)); + }), + ) + .separator() + .item( + PopupMenuItem::new("Exit") + .on_click(move |_, _, cx| { + v_exit.update(cx, |_, cx| cx.emit(ToolbarEvent::Exit)); + }), + ) + }) + }) + // Edit 菜单 + .child( + Button::new("menu-edit") + .small() + .ghost() + .label("Edit") + .dropdown_menu(|menu, _window, _cx| { + menu.item(PopupMenuItem::new("Undo (Ctrl+Z)")) + .item(PopupMenuItem::new("Redo (Ctrl+Y)")) + .separator() + .item(PopupMenuItem::new("Cut (Ctrl+X)")) + .item(PopupMenuItem::new("Copy (Ctrl+C)")) + .item(PopupMenuItem::new("Paste (Ctrl+V)")) + }), + ) + // View 菜单 + .child({ + let v = view.clone(); + Button::new("menu-view") + .small() + .ghost() + .label("View") + .dropdown_menu(move |menu, _window, _cx| { + let v_sidebar = v.clone(); + let v_term = v.clone(); + let v_refresh = v.clone(); + + menu.item( + PopupMenuItem::new("Toggle Sidebar (Ctrl+B)") + .on_click(move |_, _, cx| { + v_sidebar.update(cx, |_, cx| { + cx.emit(ToolbarEvent::ToggleSidebar) + }); + }), + ) + .item( + PopupMenuItem::new("Toggle Terminal (Ctrl+`)") + .on_click(move |_, _, cx| { + v_term.update(cx, |_, cx| { + cx.emit(ToolbarEvent::ToggleTerminal) + }); + }), + ) + .separator() + .item( + PopupMenuItem::new("Refresh Workspace") + .on_click(move |_, _, cx| { + v_refresh.update(cx, |_, cx| { + cx.emit(ToolbarEvent::RefreshWorkspace) + }); + }), + ) + }) + }) + // Run 菜单 + .child({ + let v = view.clone(); + Button::new("menu-run") + .small() + .ghost() + .label("Run") + .dropdown_menu(move |menu, _window, _cx| { + let v_run = v.clone(); + let v_debug = v.clone(); + + menu.item( + PopupMenuItem::new("Run Configuration (Shift+F10)") + .on_click(move |_, _, cx| { + v_run.update(cx, |_, cx| cx.emit(ToolbarEvent::Run)); + }), + ) + .item( + PopupMenuItem::new("Debug Configuration (Shift+F9)") + .on_click(move |_, _, cx| { + v_debug.update(cx, |_, cx| cx.emit(ToolbarEvent::Debug)); + }), + ) + }) + }) + // Terminal 菜单 + .child({ + let v = view.clone(); + Button::new("menu-terminal") + .small() + .ghost() + .label("Terminal") + .dropdown_menu(move |menu, _window, _cx| { + let v_toggle = v.clone(); + let v_clear = v.clone(); + + menu.item( + PopupMenuItem::new("Toggle Terminal") + .on_click(move |_, _, cx| { + v_toggle.update(cx, |_, cx| { + cx.emit(ToolbarEvent::ToggleTerminal) + }); + }), + ) + .item( + PopupMenuItem::new("Clear Output") + .on_click(move |_, _, cx| { + v_clear.update(cx, |_, cx| { + cx.emit(ToolbarEvent::ClearTerminal) + }); + }), + ) + }) + }) + // Help 菜单 + .child({ + let v = view.clone(); + Button::new("menu-help") + .small() + .ghost() + .label("Help") + .dropdown_menu(move |menu, _window, _cx| { + let v_about = v.clone(); + + menu.item( + PopupMenuItem::new("About Lithe") + .on_click(move |_, _, cx| { + v_about.update(cx, |_, cx| cx.emit(ToolbarEvent::About)); + }), + ) + }) + }) + // 分隔竖线 .child( div() + .h(px(14.0)) + .w(px(1.0)) + .bg(ThemeColors::border()) + .mx_2(), + ) + // 项目选择器外观 + .child( + h_flex() + .items_center() + .gap_1() .px_2() .py(px(2.0)) - .rounded_sm() - .bg(rgb(0x1e2030)) + .rounded_md() + .bg(ThemeColors::bg_tab_hover()) + .border_1() + .border_color(ThemeColors::border()) + .child("📂") + .child( + div() + .text_xs() + .text_color(ThemeColors::text_primary()) + .child(self.workspace_name.clone()), + ), + ) + // Git 分支标识 + .child( + h_flex() + .items_center() + .gap_1() + .px_2() + .py(px(2.0)) + .rounded_md() + .bg(ThemeColors::bg_tab_hover()) + .border_1() + .border_color(ThemeColors::border()) + .child("⎇") + .child( + div() + .text_xs() + .text_color(ThemeColors::accent_green()) + .child(branch), + ), + ), + ) + .child( + // 2. 中间:全局搜索栏 (Search Everywhere) + div() + .id("search-everywhere-bar") + .w(px(260.0)) + .h(px(26.0)) + .bg(ThemeColors::bg_tab_active()) + .border_1() + .border_color(ThemeColors::border()) + .rounded_md() + .px_2() + .flex() + .items_center() + .justify_between() + .cursor_pointer() + .hover(|h| h.border_color(ThemeColors::accent_blue())) + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(ToolbarEvent::QuickOpen); + })) + .child( + h_flex() + .items_center() + .gap_2() + .child(div().text_xs().text_color(ThemeColors::text_muted()).child("🔍")) + .child( + div() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child("Search files, symbols..."), + ), + ) + .child( + div() .text_xs() - .text_color(rgb(0x10b981)) - .child(format!("⎇ {}", branch)), + .text_color(ThemeColors::text_muted()) + .px_1() + .bg(ThemeColors::bg_titlebar()) + .rounded_sm() + .child("Ctrl+P"), ), ) .child( - // 中间:运行与调试配置 + // 3. 右侧:运行/调试配置按钮组 h_flex() .items_center() .gap_2() @@ -94,15 +364,9 @@ impl Render for ToolbarView { .ghost() .label("🐞 Debug") .on_click(cx.listener(|_this, _event, _window, cx| { - cx.emit(ToolbarEvent::Run); + cx.emit(ToolbarEvent::Debug); })), - ), - ) - .child( - // 右侧:保存与面板切换 - h_flex() - .items_center() - .gap_2() + ) .child( Button::new("tb-save") .small() @@ -111,15 +375,6 @@ impl Render for ToolbarView { .on_click(cx.listener(|_this, _event, _window, cx| { cx.emit(ToolbarEvent::Save); })), - ) - .child( - Button::new("tb-terminal") - .small() - .ghost() - .label("⌨ Terminal") - .on_click(cx.listener(|_this, _event, _window, cx| { - cx.emit(ToolbarEvent::ToggleTerminal); - })), ), ) } diff --git a/linux/src/workbench/view.rs b/linux/src/workbench/view.rs index 3170f8666..044ec4e5a 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -1,27 +1,37 @@ -use gpui_kit::component::{h_flex, v_flex, ActiveTheme as _}; +use gpui_kit::component::{h_flex, v_flex}; +use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::{ - div, px, rgb, AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, + div, px, AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, Styled as _, Subscription, Window, }; use crate::core::CoreClient; +use crate::theme::ThemeColors; +use crate::workbench::activity_rail::{ActivityRailEvent, ActivityRailView, ActivityTab}; use crate::workbench::bottom_panel::BottomPanelView; use crate::workbench::editor::EditorView; -use crate::workbench::sidebar::{SidebarEvent, SidebarView}; +use crate::workbench::sidebar::{SidebarEvent, SidebarTab, SidebarView}; +use crate::workbench::status_bar::StatusBarView; use crate::workbench::toolbar::{ToolbarEvent, ToolbarView}; -/// Linux 前端主工作台视图组件 +/// Linux 前端主工作台视图组件(复刻 Tauri IDE 经典架构) pub struct WorkbenchView { /// 项目工作区根路径 pub workspace_root: String, - /// 顶部工具栏 + /// 侧边栏是否展开 + pub sidebar_visible: bool, + /// 顶部标题栏/工具栏 pub toolbar: Entity, - /// 侧边栏视图实体 + /// 左侧垂直活动栏 (Activity Rail) + pub activity_rail: Entity, + /// 侧边栏面板 (Files / Git / Search) pub sidebar: Entity, - /// 编辑器视图实体 + /// 主代码编辑器区 pub editor: Entity, - /// 底部面板视图实体 + /// 底部抽屉面板 (Terminal / Output / Problems) pub bottom_panel: Entity, + /// 底部状态栏 (Status Bar) + pub status_bar: Entity, client: CoreClient, _subscriptions: Vec, } @@ -40,57 +50,170 @@ impl WorkbenchView { _window: &mut Window, cx: &mut Context, ) -> Self { - let root_for_toolbar = workspace_root.clone(); - let root_for_sidebar = workspace_root.clone(); - let root_for_editor = workspace_root.clone(); - let root_for_bottom = workspace_root.clone(); + let root = workspace_root.clone(); - let toolbar = cx.new(|_cx| ToolbarView::new(&root_for_toolbar)); - let sidebar = cx.new(|cx| SidebarView::new(root_for_sidebar, cx)); - let editor = cx.new(|cx| EditorView::new(root_for_editor, cx)); - let bottom_panel = cx.new(|cx| BottomPanelView::new(root_for_bottom, cx)); + let toolbar = cx.new(|_cx| ToolbarView::new(&root)); + let activity_rail = cx.new(|_cx| ActivityRailView::new()); + let sidebar = cx.new(|cx| SidebarView::new(root.clone(), cx)); + let editor = cx.new(|cx| EditorView::new(root.clone(), cx)); + let bottom_panel = cx.new(|cx| BottomPanelView::new(root.clone(), cx)); + let status_bar = cx.new(|_cx| StatusBarView::new()); - // 1. 订阅侧边栏事件:点击文件打开 - let sub_sidebar = cx.subscribe(&sidebar, |this, _sidebar, event: &SidebarEvent, cx| { + // 1. 订阅侧边栏文件打开事件 + let status_bar_clone = status_bar.clone(); + let sub_sidebar = cx.subscribe(&sidebar, move |this, _sidebar, event: &SidebarEvent, cx| { match event { SidebarEvent::OpenFile(path) => { this.open_file(path, cx); + let _ = status_bar_clone.update(cx, |sb, cx| { + sb.set_file_info( + Some(path.clone()), + 1, + 1, + EditorView::language_name(path).to_string(), + cx, + ); + }); + } + } + }); + + // 2. 订阅左侧活动栏 (Activity Rail) 事件 + let sidebar_clone = sidebar.clone(); + let bottom_panel_clone = bottom_panel.clone(); + let sub_rail = cx.subscribe(&activity_rail, move |this, rail, event: &ActivityRailEvent, cx| { + match event { + ActivityRailEvent::SelectTab(tab) => { + let sb_tab = match tab { + ActivityTab::Explorer => SidebarTab::Explorer, + ActivityTab::Git => SidebarTab::Git, + ActivityTab::Search => SidebarTab::Search, + }; + + let was_active = this.sidebar_visible + && rail.read(cx).active_tab == Some(*tab); + + if was_active { + this.sidebar_visible = false; + let _ = rail.update(cx, |r, cx| r.set_active_tab(None, cx)); + } else { + this.sidebar_visible = true; + let _ = rail.update(cx, |r, cx| r.set_active_tab(Some(*tab), cx)); + let _ = sidebar_clone.update(cx, |sb, cx| sb.set_tab(sb_tab, cx)); + } + cx.notify(); + } + ActivityRailEvent::ToggleTerminal => { + let _ = bottom_panel_clone.update(cx, |bp, cx| { + bp.toggle_collapsed(cx); + }); + } + ActivityRailEvent::OpenSettings => { + let _ = bottom_panel_clone.update(cx, |bp, cx| { + bp.append_log("[Settings] Settings dialog requested".to_string(), cx); + }); } } }); - // 2. 订阅工具栏事件:保存、运行、终端切换 + // 3. 订阅顶部标题栏与菜单栏事件 let sub_toolbar = cx.subscribe(&toolbar, |this, _toolbar, event: &ToolbarEvent, cx| { match event { + ToolbarEvent::NewFile => { + let _ = this.editor.update(cx, |ed, cx| { + ed.open_file("untitled.txt".to_string(), String::new(), cx); + }); + } ToolbarEvent::Save => { let _ = this.editor.update(cx, |ed, cx| { ed.save_active(cx); }); } + ToolbarEvent::CloseTab => { + let _ = this.editor.update(cx, |ed, cx| { + if let Some(idx) = ed.active_tab_index { + ed.close_tab(idx, cx); + } + }); + } + ToolbarEvent::ToggleSidebar => { + this.sidebar_visible = !this.sidebar_visible; + let tab = if this.sidebar_visible { + Some(ActivityTab::Explorer) + } else { + None + }; + let _ = this.activity_rail.update(cx, |r, cx| r.set_active_tab(tab, cx)); + cx.notify(); + } ToolbarEvent::ToggleTerminal => { let _ = this.bottom_panel.update(cx, |bp, cx| { bp.toggle_collapsed(cx); }); } + ToolbarEvent::ClearTerminal => { + let _ = this.bottom_panel.update(cx, |bp, cx| { + let _ = bp.terminal.update(cx, |term, cx| { + term.clear(cx); + }); + }); + } + ToolbarEvent::RefreshWorkspace => { + let _ = this.sidebar.update(cx, |sb, cx| { + sb.refresh(cx); + sb.refresh_git(cx); + }); + } + ToolbarEvent::QuickOpen => { + let _ = this.sidebar.update(cx, |sb, cx| { + sb.set_tab(SidebarTab::Search, cx); + }); + this.sidebar_visible = true; + let _ = this.activity_rail.update(cx, |r, cx| { + r.set_active_tab(Some(ActivityTab::Search), cx) + }); + cx.notify(); + } ToolbarEvent::Run => { let _ = this.bottom_panel.update(cx, |bp, cx| { - bp.append_log("[Run] Executing run configuration...".to_string(), cx); + bp.append_log("[Run] Executing default run configuration...".to_string(), cx); let _ = bp.terminal.update(cx, |term, cx| { term.send_command("echo '[Lithe Run]' && ls -la", cx); }); }); } + ToolbarEvent::Debug => { + let _ = this.bottom_panel.update(cx, |bp, cx| { + bp.append_log("[Debug] Launching DAP debug session...".to_string(), cx); + }); + } + ToolbarEvent::Stop => { + let _ = this.bottom_panel.update(cx, |bp, cx| { + bp.append_log("[Stop] Session terminated by user.".to_string(), cx); + }); + } + ToolbarEvent::About => { + let _ = this.bottom_panel.update(cx, |bp, cx| { + bp.append_log("[About] Lithe IDE for Linux (Powered by Zed GPUI Kit & Rust Core)".to_string(), cx); + }); + } + ToolbarEvent::Exit => { + cx.quit(); + } } }); Self { workspace_root, + sidebar_visible: true, toolbar, + activity_rail, sidebar, editor, bottom_panel, + status_bar, client: CoreClient::new(), - _subscriptions: vec![sub_sidebar, sub_toolbar], + _subscriptions: vec![sub_sidebar, sub_rail, sub_toolbar], } } @@ -123,40 +246,49 @@ impl WorkbenchView { } impl Render for WorkbenchView { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { v_flex() .size_full() - .bg(cx.theme().background) + .bg(ThemeColors::bg_editor()) .child( - // 1. 顶部工具栏 + // 1. 顶部标题栏/菜单栏/运行控制 (Title Bar) self.toolbar.clone(), ) .child( - // 2. 主体区域(左右布局) + // 2. 主工作区区域(ActivityRail + 侧边栏 + 编辑器主岛) h_flex() .flex_1() .w_full() .child( - // 左侧分栏:侧边栏(Explorer / Search / Git) - div() - .w(px(280.0)) - .h_full() - .flex_shrink_0() - .border_r_1() - .border_color(rgb(0x23263b)) - .child(self.sidebar.clone()), + // 左侧垂直图标活动栏 (Activity Rail) + self.activity_rail.clone(), ) + .when(self.sidebar_visible, |layout| { + layout.child( + // 侧边栏内容面板 (Explorer / Search / Git) + div() + .w(px(260.0)) + .h_full() + .flex_shrink_0() + .child(self.sidebar.clone()), + ) + }) .child( - // 右侧分栏:多标签代码编辑器 + // 中央编辑器主岛 div() .flex_1() .h_full() + .bg(ThemeColors::bg_editor()) .child(self.editor.clone()), ), ) .child( - // 3. 底部面板(终端 / 日志 / 问题) + // 3. 底部抽屉面板 (Terminal / Output / Problems) self.bottom_panel.clone(), ) + .child( + // 4. 最底部状态栏 (Status Bar) + self.status_bar.clone(), + ) } } From 65a7ec199a59a449b1ca4670f1f52dc2a82221dc Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 00:18:56 +0800 Subject: [PATCH 03/59] =?UTF-8?q?feat(linux):=20=E5=AF=B9=E9=BD=90=20macOS?= =?UTF-8?q?=20UI/UX=20=E4=BD=93=E9=AA=8C=E5=B9=B6=E5=B0=86=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E7=BF=BB=E8=AF=91=E4=B8=BA=20gpui-kit=20=E5=8E=9F?= =?UTF-8?q?=E7=94=9F=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除界面中所有 Emoji 字符,全面接入 gpui-kit Lucide 图标体系 - 顶部工具栏重构:实现 Lithe 品牌标识、项目与分支切换胶囊、Search Everywhere 搜索条与 Run/Debug 控制组 - 活动栏重构:对齐 macOS ActivityBar 布局,规范 32x32 图标按钮与激活态指示 - 侧边栏重构:精简工具窗口顶部标题栏,重写 Explorer/Git/Search 视图列表与状态指示徽章 - 多标签代码区重构:对齐 macOS 激活 Tab 2px 蓝条下划线、面包屑导航条、右对齐行号槽与居中空状态 - 底栏与状态栏重构:接入 Terminal/Output/Problems 标签与清理/折叠操作,完善状态栏展示 --- linux/src/theme.rs | 23 ++- linux/src/workbench/activity_rail.rs | 70 ++++--- linux/src/workbench/bottom_panel.rs | 112 ++++++++--- linux/src/workbench/editor.rs | 244 ++++++++++++++++++----- linux/src/workbench/sidebar.rs | 276 ++++++++++++++++---------- linux/src/workbench/status_bar.rs | 95 +++++++-- linux/src/workbench/toolbar.rs | 277 ++++++++++++--------------- linux/src/workbench/view.rs | 30 ++- 8 files changed, 738 insertions(+), 389 deletions(-) diff --git a/linux/src/theme.rs b/linux/src/theme.rs index 6b9bacdf0..73856b750 100644 --- a/linux/src/theme.rs +++ b/linux/src/theme.rs @@ -1,4 +1,8 @@ use gpui_kit::Rgba; +#[allow(unused_imports)] +pub use gpui_kit::assets::IconName; +#[allow(unused_imports)] +pub use gpui_kit::component::Icon; pub const fn const_rgb(hex: u32) -> Rgba { let r = ((hex >> 16) & 0xFF) as f32 / 255.0; @@ -17,14 +21,17 @@ impl ThemeColors { pub const BG_EDITOR: Rgba = const_rgb(0x2b2d30); pub const BG_TAB_BAR: Rgba = const_rgb(0x1e1f22); pub const BG_TAB_ACTIVE: Rgba = const_rgb(0x2b2d30); - pub const BG_TAB_HOVER: Rgba = const_rgb(0x24262b); + pub const BG_TAB_HOVER: Rgba = const_rgb(0x26282c); pub const BG_BOTTOM_PANEL: Rgba = const_rgb(0x1e1f22); pub const BG_STATUSBAR: Rgba = const_rgb(0x18191c); - pub const BORDER: Rgba = const_rgb(0x393b40); + pub const BORDER: Rgba = const_rgb(0x323438); pub const ACCENT_BLUE: Rgba = const_rgb(0x3574f0); pub const ACCENT_GREEN: Rgba = const_rgb(0x39a36b); + pub const ACCENT_YELLOW: Rgba = const_rgb(0xd29922); + pub const ACCENT_RED: Rgba = const_rgb(0xe02e2a); pub const TEXT_PRIMARY: Rgba = const_rgb(0xdfdfe0); pub const TEXT_MUTED: Rgba = const_rgb(0x818594); + pub const SUBTLE_SELECTION: Rgba = const_rgb(0x26282d); #[inline] pub fn bg_titlebar() -> Rgba { @@ -75,6 +82,14 @@ impl ThemeColors { Self::ACCENT_GREEN } #[inline] + pub fn accent_yellow() -> Rgba { + Self::ACCENT_YELLOW + } + #[inline] + pub fn accent_red() -> Rgba { + Self::ACCENT_RED + } + #[inline] pub fn text_primary() -> Rgba { Self::TEXT_PRIMARY } @@ -82,4 +97,8 @@ impl ThemeColors { pub fn text_muted() -> Rgba { Self::TEXT_MUTED } + #[inline] + pub fn subtle_selection() -> Rgba { + Self::SUBTLE_SELECTION + } } diff --git a/linux/src/workbench/activity_rail.rs b/linux/src/workbench/activity_rail.rs index a8279f5d9..03a007567 100644 --- a/linux/src/workbench/activity_rail.rs +++ b/linux/src/workbench/activity_rail.rs @@ -1,8 +1,9 @@ +use gpui_kit::assets::IconName; +use gpui_kit::component::button::{Button, ButtonVariants as _}; use gpui_kit::component::v_flex; use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::{ - div, px, Context, EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _, - Render, StatefulInteractiveElement as _, Styled as _, Window, + px, Context, EventEmitter, IntoElement, ParentElement as _, Render, Styled as _, Window, }; use crate::theme::ThemeColors; @@ -45,7 +46,7 @@ impl Render for ActivityRailView { let active = self.active_tab; v_flex() - .w(px(42.0)) + .w(px(40.0)) .h_full() .bg(ThemeColors::bg_activity_rail()) .border_r_1() @@ -61,45 +62,46 @@ impl Render for ActivityRailView { .gap_1() .child(self.render_rail_button( "rail-files", - "📁", - "Files (Project Explorer)", + IconName::Folder, + "Explorer", active == Some(ActivityTab::Explorer), ActivityRailEvent::SelectTab(ActivityTab::Explorer), cx, )) .child(self.render_rail_button( "rail-git", - "⎇", - "Version Control (Git)", + IconName::GitBranch, + "Version Control", active == Some(ActivityTab::Git), ActivityRailEvent::SelectTab(ActivityTab::Git), cx, )) .child(self.render_rail_button( "rail-search", - "🔍", + IconName::Search, "Search Everywhere", active == Some(ActivityTab::Search), ActivityRailEvent::SelectTab(ActivityTab::Search), cx, - )) - .child(self.render_rail_button( - "rail-terminal", - "⌨", - "Terminal", - false, - ActivityRailEvent::ToggleTerminal, - cx, )), ) .child( - // 底部设置图标 + // 底部工具按钮列表 v_flex() .w_full() .items_center() + .gap_1() + .child(self.render_rail_button( + "rail-terminal", + IconName::Terminal, + "Terminal", + false, + ActivityRailEvent::ToggleTerminal, + cx, + )) .child(self.render_rail_button( "rail-settings", - "⚙", + IconName::Settings, "Settings", false, ActivityRailEvent::OpenSettings, @@ -113,33 +115,29 @@ impl ActivityRailView { fn render_rail_button( &self, id: &'static str, - icon: &'static str, - _tooltip: &'static str, + icon: IconName, + tooltip: &'static str, is_active: bool, event: ActivityRailEvent, cx: &mut Context, ) -> impl IntoElement { - div() - .id(id) - .w(px(34.0)) - .h(px(34.0)) - .rounded_md() - .flex() - .items_center() - .justify_center() - .cursor_pointer() - .text_sm() + Button::new(id) + .ghost() + .icon(icon) + .tooltip(tooltip) + .w(px(32.0)) + .h(px(32.0)) + .rounded(px(4.0)) + .text_color(if is_active { + ThemeColors::accent_blue() + } else { + ThemeColors::text_muted() + }) .when(is_active, |btn| { btn.bg(ThemeColors::bg_sidebar()) - .text_color(ThemeColors::accent_blue()) .border_l_2() .border_color(ThemeColors::accent_blue()) }) - .when(!is_active, |btn| { - btn.text_color(ThemeColors::text_muted()) - .hover(|h| h.bg(ThemeColors::bg_tab_hover()).text_color(ThemeColors::text_primary())) - }) - .child(icon) .on_click(cx.listener(move |_this, _event, _window, cx| { cx.emit(event.clone()); })) diff --git a/linux/src/workbench/bottom_panel.rs b/linux/src/workbench/bottom_panel.rs index 8fea5f41a..711d70dc9 100644 --- a/linux/src/workbench/bottom_panel.rs +++ b/linux/src/workbench/bottom_panel.rs @@ -1,6 +1,7 @@ +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, Sizable 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, InteractiveElement as _, IntoElement, @@ -57,16 +58,18 @@ impl BottomPanelView { fn render_tab_button( &self, id: &'static str, + icon: IconName, label: String, is_active: bool, tab: BottomTab, cx: &mut Context, ) -> impl IntoElement { - div() + h_flex() .id(id) + .items_center() + .gap_1p5() .px_3() - .py_1() - .rounded_t_sm() + .h(px(30.0)) .cursor_pointer() .text_xs() .when(is_active, |btn| { @@ -77,8 +80,20 @@ impl BottomPanelView { }) .when(!is_active, |btn| { btn.text_color(ThemeColors::text_muted()) - .hover(|h| h.bg(ThemeColors::bg_tab_hover()).text_color(ThemeColors::text_primary())) + .hover(|h| { + h.bg(ThemeColors::bg_tab_hover()) + .text_color(ThemeColors::text_primary()) + }) }) + .child( + Icon::new(icon) + .size(px(13.0)) + .text_color(if is_active { + ThemeColors::accent_blue() + } else { + ThemeColors::text_muted() + }), + ) .child(label) .on_click(cx.listener(move |this, _event, _window, cx| { this.set_tab(tab, cx); @@ -92,24 +107,40 @@ impl Render for BottomPanelView { return div() .h(px(26.0)) .w_full() - .bg(ThemeColors::BG_STATUSBAR) + .bg(ThemeColors::bg_statusbar()) .border_t_1() - .border_color(ThemeColors::BORDER) + .border_color(ThemeColors::border()) .flex() .items_center() .justify_between() .px_3() .child( - div() - .text_xs() - .text_color(ThemeColors::TEXT_MUTED) - .child("Panel collapsed"), + h_flex() + .id("collapsed-terminal-btn") + .items_center() + .gap_1p5() + .cursor_pointer() + .child( + Icon::new(IconName::Terminal) + .size(px(12.0)) + .text_color(ThemeColors::text_muted()), + ) + .child( + div() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child("Terminal"), + ) + .on_click(cx.listener(|this, _event, _window, cx| { + this.toggle_collapsed(cx); + })), ) .child( Button::new("expand-panel") .small() .ghost() - .label("▲ Expand") + .icon(IconName::ChevronUp) + .tooltip("Expand Panel") .on_click(cx.listener(|this, _event, _window, cx| { this.toggle_collapsed(cx); })), @@ -119,17 +150,17 @@ impl Render for BottomPanelView { v_flex() .h(px(240.0)) .w_full() - .bg(ThemeColors::BG_BOTTOM_PANEL) + .bg(ThemeColors::bg_bottom_panel()) .border_t_1() - .border_color(ThemeColors::BORDER) + .border_color(ThemeColors::border()) .child( - // 顶部 Tab 切换栏 + // 顶部 Tab 切换栏(高 30px) h_flex() - .h(px(32.0)) + .h(px(30.0)) .w_full() - .bg(ThemeColors::BG_TAB_BAR) + .bg(ThemeColors::bg_tab_bar()) .border_b_1() - .border_color(ThemeColors::BORDER) + .border_color(ThemeColors::border()) .items_center() .justify_between() .px_2() @@ -139,6 +170,7 @@ impl Render for BottomPanelView { .gap_1() .child(self.render_tab_button( "tab-terminal", + IconName::Terminal, "Terminal".to_string(), self.active_tab == BottomTab::Terminal, BottomTab::Terminal, @@ -146,6 +178,7 @@ impl Render for BottomPanelView { )) .child(self.render_tab_button( "tab-output", + IconName::FileText, "Output".to_string(), self.active_tab == BottomTab::Output, BottomTab::Output, @@ -153,6 +186,7 @@ impl Render for BottomPanelView { )) .child(self.render_tab_button( "tab-problems", + IconName::Bug, format!("Problems ({})", self.problems.len()), self.active_tab == BottomTab::Problems, BottomTab::Problems, @@ -160,13 +194,41 @@ impl Render for BottomPanelView { )), ) .child( - Button::new("collapse-panel") - .small() - .ghost() - .label("▼ Hide") - .on_click(cx.listener(|this, _event, _window, cx| { - this.toggle_collapsed(cx); - })), + h_flex() + .items_center() + .gap_1() + .child( + Button::new("clear-panel") + .small() + .ghost() + .icon(IconName::Trash) + .tooltip("Clear") + .on_click(cx.listener(|this, _event, _window, cx| { + match this.active_tab { + BottomTab::Terminal => { + let _ = this.terminal.update(cx, |t, cx| t.clear(cx)); + } + BottomTab::Output => { + this.output_logs.clear(); + cx.notify(); + } + BottomTab::Problems => { + this.problems.clear(); + cx.notify(); + } + } + })), + ) + .child( + Button::new("collapse-panel") + .small() + .ghost() + .icon(IconName::ChevronDown) + .tooltip("Hide Panel") + .on_click(cx.listener(|this, _event, _window, cx| { + this.toggle_collapsed(cx); + })), + ), ), ) .child( diff --git a/linux/src/workbench/editor.rs b/linux/src/workbench/editor.rs index 611850fff..5049f875f 100644 --- a/linux/src/workbench/editor.rs +++ b/linux/src/workbench/editor.rs @@ -1,8 +1,9 @@ +use gpui_kit::assets::IconName; use gpui_kit::component::scroll::ScrollableElement as _; -use gpui_kit::component::{h_flex, v_flex}; +use gpui_kit::component::{h_flex, v_flex, Icon}; use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::{ - div, px, rgb, Context, InteractiveElement as _, IntoElement, ParentElement as _, + div, px, Context, FontWeight, InteractiveElement as _, IntoElement, ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Window, }; @@ -21,7 +22,7 @@ pub struct EditorTab { pub cursor_col: usize, } -/// 多标签代码编辑器组件(对齐 IntelliJ / Tauri 视觉规范) +/// 多标签代码编辑器组件(对齐 macOS LitheTheme / IntelliJ 视觉规范) pub struct EditorView { pub workspace_root: String, pub tabs: Vec, @@ -137,12 +138,40 @@ impl EditorView { "YAML" } else if path.ends_with(".sh") { "Shell" + } else if path.ends_with(".sql") { + "SQL" } else { "Plain Text" } } } +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 { let active_tab = self @@ -151,22 +180,26 @@ impl Render for EditorView { v_flex() .size_full() - .bg(ThemeColors::BG_EDITOR) + .bg(ThemeColors::bg_editor()) .child( - // 1. 顶部标签栏(Tab Bar) + // 1. 顶部标签栏(Tab Bar,高 34px) h_flex() .h(px(34.0)) .w_full() - .bg(ThemeColors::BG_TAB_BAR) + .bg(ThemeColors::bg_tab_bar()) .border_b_1() - .border_color(ThemeColors::BORDER) + .border_color(ThemeColors::border()) .items_center() .overflow_x_scrollbar() - .px_1() .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; + let icon_name = if is_code_file(&title) { + IconName::FileCode + } else { + IconName::FileText + }; h_flex() .id(idx) @@ -174,72 +207,124 @@ impl Render for EditorView { .items_center() .gap_2() .px_3() + .relative() .cursor_pointer() - .when(is_active, |t| { - t.bg(ThemeColors::BG_TAB_ACTIVE) - .border_t_2() - .border_color(ThemeColors::ACCENT_BLUE) - }) + .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)) + 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( + Icon::new(icon_name) + .size(px(14.0)) + .text_color(if is_active { + ThemeColors::accent_blue() + } else { + ThemeColors::text_muted() + }), + ) .child( div() .text_xs() .text_color(if is_active { - ThemeColors::TEXT_PRIMARY + ThemeColors::text_primary() } else { - ThemeColors::TEXT_MUTED + ThemeColors::text_muted() }) .child(title), ) .when(is_dirty, |t| { t.child( div() - .text_xs() - .text_color(ThemeColors::ACCENT_BLUE) - .child("●"), + .w(px(6.0)) + .h(px(6.0)) + .rounded_full() + .bg(ThemeColors::accent_blue()), ) }) .child( div() .id(("close-tab", idx)) - .text_xs() - .text_color(ThemeColors::TEXT_MUTED) - .hover(|h| h.text_color(rgb(0xef4444))) - .child("×") + .p(px(2.0)) + .rounded_sm() + .cursor_pointer() + .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()), + ) + }) })), ) .when_some(active_tab.as_ref(), |this, tab| { - // 2. 面包屑导航栏(Breadcrumb Bar) + // 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) + .bg(ThemeColors::bg_tab_active()) .border_b_1() - .border_color(ThemeColors::BORDER) + .border_color(ThemeColors::border()) .items_center() .px_3() - .gap_1() + .gap_1p5() .text_xs() - .text_color(ThemeColors::TEXT_MUTED) - .child("📂") + .text_color(ThemeColors::text_muted()) .child( - tab.path - .split('/') - .collect::>() - .join(" › "), - ), + 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( @@ -247,27 +332,33 @@ impl Render for EditorView { div().flex_1().w_full().overflow_scrollbar().child( if let Some(tab) = &active_tab { let lines: Vec = tab.content.lines().map(|s| s.to_string()).collect(); + let total_lines = lines.len().max(1); h_flex() .size_full() - .p_3() - .gap_4() + .p_2() .font_family("monospace") .text_xs() .child( // 代码行号槽(Gutter) v_flex() .flex_shrink_0() - .text_color(ThemeColors::TEXT_MUTED) - .children((1..=lines.len().max(1)).map(|num| { - div().h(px(20.0)).child(format!("{num:4}")) + .w(px(48.0)) + .pr_3() + .border_r_1() + .border_color(ThemeColors::border()) + .text_color(ThemeColors::text_muted()) + .items_end() + .children((1..=total_lines).map(|num| { + div().h(px(20.0)).child(format!("{num}")) })), ) .child( // 代码文本展示行 v_flex() .flex_1() - .text_color(ThemeColors::TEXT_PRIMARY) + .pl_3() + .text_color(ThemeColors::text_primary()) .children(lines.into_iter().enumerate().map(|(idx, line)| { div() .id(idx) @@ -276,14 +367,77 @@ impl Render for EditorView { })), ) } else { - // 无打开文件欢迎占位 + // 无打开文件 Empty State(精致 Lithe 居中徽标与操作提示) h_flex() .size_full() .items_center() .justify_center() - .text_sm() - .text_color(ThemeColors::TEXT_MUTED) - .child("No file opened. Select a file from the project explorer or press Ctrl+P.") + .child( + v_flex() + .items_center() + .gap_3() + .child( + Icon::new(IconName::Zap) + .size(px(48.0)) + .text_color(ThemeColors::accent_blue()), + ) + .child( + div() + .text_lg() + .font_weight(FontWeight::BOLD) + .text_color(ThemeColors::text_primary()) + .child("Lithe IDEA"), + ) + .child( + div() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child("Next-generation IDE for Linux, powered by GPUI Kit & Rust Core"), + ) + .child( + v_flex() + .pt_2() + .gap_2() + .child( + h_flex() + .items_center() + .gap_2() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child("Search files everywhere:") + .child( + div() + .px_1p5() + .py(px(1.0)) + .bg(ThemeColors::bg_tab_hover()) + .border_1() + .border_color(ThemeColors::border()) + .rounded_sm() + .text_color(ThemeColors::text_primary()) + .child("Ctrl+P"), + ), + ) + .child( + h_flex() + .items_center() + .gap_2() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child("Toggle terminal panel:") + .child( + div() + .px_1p5() + .py(px(1.0)) + .bg(ThemeColors::bg_tab_hover()) + .border_1() + .border_color(ThemeColors::border()) + .rounded_sm() + .text_color(ThemeColors::text_primary()) + .child("Ctrl+`"), + ), + ), + ), + ) }, ), ) diff --git a/linux/src/workbench/sidebar.rs b/linux/src/workbench/sidebar.rs index a2eb22e46..b69af9ac1 100644 --- a/linux/src/workbench/sidebar.rs +++ b/linux/src/workbench/sidebar.rs @@ -1,8 +1,10 @@ +use gpui_kit::assets::IconName; use gpui_kit::component::button::{Button, ButtonVariants as _}; -use gpui_kit::component::{h_flex, v_flex, Sizable 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, rgb, Context, EventEmitter, InteractiveElement as _, IntoElement, + div, px, Context, EventEmitter, FontWeight, InteractiveElement as _, IntoElement, ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Window, }; use serde::{Deserialize, Serialize}; @@ -327,88 +329,71 @@ impl SidebarView { } false } +} - fn render_tab_button( - &self, - id: &'static str, - label: String, - is_active: bool, - tab: SidebarTab, - cx: &mut Context, - ) -> impl IntoElement { - div() - .id(id) - .px_3() - .py_1() - .rounded_t_sm() - .cursor_pointer() - .text_xs() - .when(is_active, |btn| { - btn.bg(ThemeColors::bg_sidebar()) - .text_color(ThemeColors::text_primary()) - .border_b_2() - .border_color(ThemeColors::accent_blue()) - }) - .when(!is_active, |btn| { - btn.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.set_tab(tab, cx); - })) - } +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 SidebarView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let title = match self.active_tab { + SidebarTab::Explorer => "PROJECT", + SidebarTab::Search => "SEARCH", + SidebarTab::Git => "GIT", + }; + v_flex() .size_full() - .bg(ThemeColors::BG_SIDEBAR) + .bg(ThemeColors::bg_sidebar()) .border_r_1() - .border_color(ThemeColors::BORDER) + .border_color(ThemeColors::border()) .child( - // 顶部视图切换栏 (Explorer / Search / Git) + // 顶部工具窗口精简标题 + 刷新操作按钮(无老旧横排Tab栏) h_flex() - .h(px(36.0)) + .h(px(32.0)) .w_full() - .bg(ThemeColors::BG_TAB_BAR) + .bg(ThemeColors::bg_sidebar()) .border_b_1() - .border_color(ThemeColors::BORDER) + .border_color(ThemeColors::border()) .items_center() .justify_between() - .px_2() + .px_3() .child( - h_flex() - .items_center() - .gap_1() - .child(self.render_tab_button( - "tab-explorer", - "Files".to_string(), - self.active_tab == SidebarTab::Explorer, - SidebarTab::Explorer, - cx, - )) - .child(self.render_tab_button( - "tab-search", - "Search".to_string(), - self.active_tab == SidebarTab::Search, - SidebarTab::Search, - cx, - )) - .child(self.render_tab_button( - "tab-git", - format!("Git ({})", self.git_changes.len()), - self.active_tab == SidebarTab::Git, - SidebarTab::Git, - cx, - )), + div() + .text_xs() + .font_weight(FontWeight::BOLD) + .text_color(ThemeColors::text_muted()) + .child(title), ) .child( - Button::new("refresh-btn") + Button::new("sidebar-refresh") .small() .ghost() - .label("↻") + .icon(IconName::RotateCw) + .tooltip("Refresh") .on_click(cx.listener(|this, _event, _window, cx| { this.refresh(cx); this.refresh_git(cx); @@ -417,7 +402,7 @@ impl Render for SidebarView { ) .child( // 对应面板内容渲染 - div().flex_1().w_full().child( + div().flex_1().w_full().overflow_y_scrollbar().child( match self.active_tab { SidebarTab::Explorer => self.render_explorer(cx).into_any_element(), SidebarTab::Search => self.render_search(cx).into_any_element(), @@ -437,13 +422,12 @@ impl SidebarView { v_flex() .size_full() - .p_2() - .gap_1() + .py_1() .when(self.is_loading, |this| { this.child( div() .text_xs() - .text_color(rgb(0x8a90a2)) + .text_color(ThemeColors::text_muted()) .p_2() .child("Loading workspace files..."), ) @@ -452,7 +436,7 @@ impl SidebarView { this.child( div() .text_xs() - .text_color(rgb(0xef4444)) + .text_color(ThemeColors::accent_red()) .p_2() .child(err.clone()), ) @@ -470,21 +454,73 @@ impl SidebarView { .items_center() .cursor_pointer() .rounded_sm() - .pl(px(indent + 4.0)) + .pl(px(indent + 6.0)) .pr_2() + .gap_1p5() .text_xs() .when(is_selected, |row| { - row.bg(ThemeColors::BG_TAB_ACTIVE).text_color(ThemeColors::TEXT_PRIMARY) + 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)) + row.text_color(ThemeColors::text_primary()) + .hover(|h| h.bg(ThemeColors::bg_tab_hover())) }) .child(if is_dir { - if item.is_expanded { "📂 " } else { "📁 " } + if item.is_expanded { + h_flex() + .items_center() + .gap_1() + .child( + Icon::new(IconName::ChevronDown) + .size(px(12.0)) + .text_color(ThemeColors::text_muted()), + ) + .child( + Icon::new(IconName::FolderOpen) + .size(px(14.0)) + .text_color(ThemeColors::accent_blue()), + ) + .into_any_element() + } else { + h_flex() + .items_center() + .gap_1() + .child( + Icon::new(IconName::ChevronRight) + .size(px(12.0)) + .text_color(ThemeColors::text_muted()), + ) + .child( + Icon::new(IconName::Folder) + .size(px(14.0)) + .text_color(ThemeColors::accent_blue()), + ) + .into_any_element() + } } else { - "📄 " + let icon_name = if is_code_file(&item.name) { + IconName::FileCode + } else { + IconName::FileText + }; + h_flex() + .items_center() + .gap_1() + .child(div().w(px(12.0))) + .child( + Icon::new(icon_name) + .size(px(14.0)) + .text_color(ThemeColors::text_muted()), + ) + .into_any_element() }) - .child(item.name) + .child( + div() + .flex_1() + .truncate() + .child(item.name), + ) .on_click(cx.listener({ let path = path_str.clone(); move |this, _event, _window, cx| { @@ -510,15 +546,21 @@ impl SidebarView { .h(px(28.0)) .w_full() .items_center() - .bg(ThemeColors::BG_TAB_ACTIVE) + .gap_2() + .bg(ThemeColors::bg_tab_active()) .border_1() - .border_color(ThemeColors::BORDER) - .rounded_sm() + .border_color(ThemeColors::border()) + .rounded(px(4.0)) .px_2() + .child( + Icon::new(IconName::Search) + .size(px(13.0)) + .text_color(ThemeColors::text_muted()), + ) .child( div() .text_xs() - .text_color(ThemeColors::TEXT_MUTED) + .text_color(ThemeColors::text_muted()) .child("Search files or text..."), ), ) @@ -526,7 +568,8 @@ impl SidebarView { this.child( div() .text_xs() - .text_color(rgb(0x8a90a2)) + .text_color(ThemeColors::text_muted()) + .p_1() .child("Searching..."), ) }) @@ -544,22 +587,31 @@ impl SidebarView { v_flex() .id(idx) - .p_1() + .p_1p5() .rounded_sm() .cursor_pointer() - .hover(|h| h.bg(ThemeColors::BG_TAB_HOVER)) + .hover(|h| h.bg(ThemeColors::bg_tab_hover())) .child( h_flex() .items_center() - .gap_1() + .gap_1p5() .text_xs() - .text_color(ThemeColors::ACCENT_BLUE) - .child(format!("{}{}", path, line_info)), + .child( + Icon::new(IconName::FileText) + .size(px(12.0)) + .text_color(ThemeColors::accent_blue()), + ) + .child( + div() + .text_color(ThemeColors::accent_blue()) + .child(format!("{}{}", path, line_info)), + ), ) .child( div() .text_xs() - .text_color(ThemeColors::TEXT_MUTED) + .text_color(ThemeColors::text_muted()) + .pl(px(18.0)) .child(preview), ) .on_click(cx.listener({ @@ -587,21 +639,41 @@ impl SidebarView { .gap_2() .pb_2() .border_b_1() - .border_color(ThemeColors::BORDER) - .text_xs() - .text_color(ThemeColors::ACCENT_GREEN) - .child("⎇ Branch:") - .child(branch), + .border_color(ThemeColors::border()) + .child( + Icon::new(IconName::GitBranch) + .size(px(13.0)) + .text_color(ThemeColors::accent_green()), + ) + .child( + div() + .text_xs() + .font_weight(FontWeight::BOLD) + .text_color(ThemeColors::accent_green()) + .child(branch), + ), ) .child( div() .text_xs() - .text_color(ThemeColors::TEXT_MUTED) + .text_color(ThemeColors::text_muted()) .child(format!("Changed Files ({})", self.git_changes.len())), ) .children(self.git_changes.iter().enumerate().map(|(idx, change)| { let path = change.path.clone(); let status = change.status.clone(); + let status_color = match status.as_str() { + "M" => ThemeColors::accent_yellow(), + "A" => ThemeColors::accent_green(), + "D" => ThemeColors::accent_red(), + _ => ThemeColors::text_muted(), + }; + + let icon_name = if is_code_file(&path) { + IconName::FileCode + } else { + IconName::FileText + }; h_flex() .id(idx) @@ -612,18 +684,28 @@ impl SidebarView { .px_2() .rounded_sm() .cursor_pointer() - .hover(|h| h.bg(ThemeColors::BG_TAB_HOVER)) + .hover(|h| h.bg(ThemeColors::bg_tab_hover())) .text_xs() .child( h_flex() .items_center() .gap_2() - .text_color(ThemeColors::TEXT_PRIMARY) - .child(path.clone()), + .child( + Icon::new(icon_name) + .size(px(13.0)) + .text_color(ThemeColors::text_muted()), + ) + .child( + div() + .text_color(ThemeColors::text_primary()) + .truncate() + .child(path.clone()), + ), ) .child( div() - .text_color(rgb(0xf59e0b)) + .font_weight(FontWeight::BOLD) + .text_color(status_color) .child(status), ) .on_click(cx.listener({ diff --git a/linux/src/workbench/status_bar.rs b/linux/src/workbench/status_bar.rs index 66a447972..b9dd03a48 100644 --- a/linux/src/workbench/status_bar.rs +++ b/linux/src/workbench/status_bar.rs @@ -1,6 +1,7 @@ -use gpui_kit::component::h_flex; +use gpui_kit::assets::IconName; +use gpui_kit::component::{h_flex, Icon}; use gpui_kit::{ - div, px, Context, IntoElement, ParentElement as _, Render, Styled as _, Window, + div, px, Context, FontWeight, IntoElement, ParentElement as _, Render, Styled as _, Window, }; use crate::theme::ThemeColors; @@ -68,42 +69,96 @@ impl Render for StatusBarView { .text_xs() .text_color(ThemeColors::text_muted()) .child( - // 左侧状态栏项目 + // 左侧状态栏项目(分支 + 文件状态) h_flex() .items_center() - .gap_3() + .gap_2() .child( h_flex() .items_center() - .gap_1() + .gap_1p5() .text_color(ThemeColors::accent_green()) - .child("⎇") - .child(branch), + .child( + Icon::new(IconName::GitBranch) + .size(px(12.0)) + .text_color(ThemeColors::accent_green()), + ) + .child( + div() + .font_weight(FontWeight::SEMIBOLD) + .child(branch), + ), ) - .child(div().text_color(ThemeColors::border()).child("|")) .child( div() - .text_color(ThemeColors::text_primary()) - .child(file_label), + .h(px(10.0)) + .w(px(1.0)) + .bg(ThemeColors::border()) + .mx_1(), + ) + .child( + h_flex() + .items_center() + .gap_1p5() + .child( + Icon::new(IconName::FileText) + .size(px(12.0)) + .text_color(ThemeColors::text_muted()), + ) + .child( + div() + .text_color(ThemeColors::text_primary()) + .child(file_label), + ), ), ) .child( - // 右侧状态栏项目 + // 右侧状态栏项目(行列号、缩进、编码、语言、核心状态) h_flex() .items_center() - .gap_3() + .gap_2() .child(format!("Ln {}, Col {}", self.cursor_line, self.cursor_col)) - .child(div().text_color(ThemeColors::border()).child("|")) - .child(self.language.clone()) - .child(div().text_color(ThemeColors::border()).child("|")) - .child(self.encoding.clone()) - .child(div().text_color(ThemeColors::border()).child("|")) + .child( + div() + .h(px(10.0)) + .w(px(1.0)) + .bg(ThemeColors::border()), + ) .child("4 spaces") - .child(div().text_color(ThemeColors::border()).child("|")) .child( div() - .text_color(ThemeColors::accent_blue()) - .child("Lithe Core"), + .h(px(10.0)) + .w(px(1.0)) + .bg(ThemeColors::border()), + ) + .child(self.encoding.clone()) + .child( + div() + .h(px(10.0)) + .w(px(1.0)) + .bg(ThemeColors::border()), + ) + .child(self.language.clone()) + .child( + div() + .h(px(10.0)) + .w(px(1.0)) + .bg(ThemeColors::border()), + ) + .child( + h_flex() + .items_center() + .gap_1() + .child( + Icon::new(IconName::Check) + .size(px(12.0)) + .text_color(ThemeColors::accent_green()), + ) + .child( + div() + .text_color(ThemeColors::accent_blue()) + .child("Lithe Core"), + ), ), ) } diff --git a/linux/src/workbench/toolbar.rs b/linux/src/workbench/toolbar.rs index 748f52b4d..781991340 100644 --- a/linux/src/workbench/toolbar.rs +++ b/linux/src/workbench/toolbar.rs @@ -1,27 +1,29 @@ +use gpui_kit::assets::IconName; use gpui_kit::component::button::{Button, ButtonVariants as _}; use gpui_kit::component::menu::{DropdownMenu as _, PopupMenuItem}; -use gpui_kit::component::{h_flex, Sizable as _}; +use gpui_kit::component::{h_flex, Icon, Sizable as _}; use gpui_kit::{ - div, px, Context, EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _, - Render, StatefulInteractiveElement as _, Styled as _, Window, + div, px, Context, EventEmitter, FontWeight, InteractiveElement as _, IntoElement, + ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Window, }; use crate::theme::ThemeColors; #[derive(Debug, Clone)] +#[allow(dead_code)] pub enum ToolbarEvent { NewFile, Save, CloseTab, Run, Debug, - #[allow(dead_code)] Stop, ToggleTerminal, ClearTerminal, ToggleSidebar, RefreshWorkspace, QuickOpen, + OpenSettings, About, Exit, } @@ -46,7 +48,6 @@ impl ToolbarView { } } - #[allow(dead_code)] pub fn set_git_branch(&mut self, branch: Option, cx: &mut Context) { self.git_branch = branch; cx.notify(); @@ -68,96 +69,78 @@ impl Render for ToolbarView { .justify_between() .px_3() .child( - // 1. 左侧:Logo + 菜单栏 + 项目名称选择器 + 分支 + // 1. 左侧:Lithe Logo + 项目选择器胶囊 + 分支选择器胶囊 h_flex() .items_center() - .gap_1() + .gap_3() .child( + // Lithe Logo + 品牌名 h_flex() .items_center() - .gap_1() - .pr_2() + .gap_1p5() + .pr_1() .child( - div() - .text_sm() - .text_color(ThemeColors::accent_blue()) - .child("⚡"), + Icon::new(IconName::Zap) + .size(px(16.0)) + .text_color(ThemeColors::accent_blue()), ) .child( div() + .font_weight(FontWeight::BOLD) .text_sm() .text_color(ThemeColors::text_primary()) .child("Lithe"), ), ) - // File 菜单 + // 项目选择器胶囊(点击可展开项目切换下拉菜单) .child({ let v = view.clone(); - Button::new("menu-file") + Button::new("tb-project-selector") .small() .ghost() - .label("File") + .icon(IconName::Folder) + .label(self.workspace_name.clone()) + .bg(ThemeColors::bg_tab_hover()) + .border_1() + .border_color(ThemeColors::border()) + .rounded_md() .dropdown_menu(move |menu, _window, _cx| { - let v_new = v.clone(); + let v_refresh = v.clone(); + let v_new_file = v.clone(); let v_save = v.clone(); let v_close = v.clone(); + let v_sidebar = v.clone(); + let v_terminal = v.clone(); + let v_settings = v.clone(); + let v_about = v.clone(); let v_exit = v.clone(); menu.item( PopupMenuItem::new("New File (Ctrl+N)") .on_click(move |_, _, cx| { - v_new.update(cx, |_, cx| cx.emit(ToolbarEvent::NewFile)); + v_new_file.update(cx, |_, cx| { + cx.emit(ToolbarEvent::NewFile) + }); }), ) .item( PopupMenuItem::new("Save (Ctrl+S)") .on_click(move |_, _, cx| { - v_save.update(cx, |_, cx| cx.emit(ToolbarEvent::Save)); + v_save.update(cx, |_, cx| { + cx.emit(ToolbarEvent::Save) + }); }), ) .item( PopupMenuItem::new("Close Active Tab (Ctrl+W)") .on_click(move |_, _, cx| { - v_close.update(cx, |_, cx| cx.emit(ToolbarEvent::CloseTab)); + v_close.update(cx, |_, cx| { + cx.emit(ToolbarEvent::CloseTab) + }); }), ) .separator() .item( - PopupMenuItem::new("Exit") - .on_click(move |_, _, cx| { - v_exit.update(cx, |_, cx| cx.emit(ToolbarEvent::Exit)); - }), - ) - }) - }) - // Edit 菜单 - .child( - Button::new("menu-edit") - .small() - .ghost() - .label("Edit") - .dropdown_menu(|menu, _window, _cx| { - menu.item(PopupMenuItem::new("Undo (Ctrl+Z)")) - .item(PopupMenuItem::new("Redo (Ctrl+Y)")) - .separator() - .item(PopupMenuItem::new("Cut (Ctrl+X)")) - .item(PopupMenuItem::new("Copy (Ctrl+C)")) - .item(PopupMenuItem::new("Paste (Ctrl+V)")) - }), - ) - // View 菜单 - .child({ - let v = view.clone(); - Button::new("menu-view") - .small() - .ghost() - .label("View") - .dropdown_menu(move |menu, _window, _cx| { - let v_sidebar = v.clone(); - let v_term = v.clone(); - let v_refresh = v.clone(); - - menu.item( PopupMenuItem::new("Toggle Sidebar (Ctrl+B)") .on_click(move |_, _, cx| { v_sidebar.update(cx, |_, cx| { @@ -168,12 +151,11 @@ impl Render for ToolbarView { .item( PopupMenuItem::new("Toggle Terminal (Ctrl+`)") .on_click(move |_, _, cx| { - v_term.update(cx, |_, cx| { + v_terminal.update(cx, |_, cx| { cx.emit(ToolbarEvent::ToggleTerminal) }); }), ) - .separator() .item( PopupMenuItem::new("Refresh Workspace") .on_click(move |_, _, cx| { @@ -182,124 +164,60 @@ impl Render for ToolbarView { }); }), ) - }) - }) - // Run 菜单 - .child({ - let v = view.clone(); - Button::new("menu-run") - .small() - .ghost() - .label("Run") - .dropdown_menu(move |menu, _window, _cx| { - let v_run = v.clone(); - let v_debug = v.clone(); - - menu.item( - PopupMenuItem::new("Run Configuration (Shift+F10)") - .on_click(move |_, _, cx| { - v_run.update(cx, |_, cx| cx.emit(ToolbarEvent::Run)); - }), - ) + .separator() .item( - PopupMenuItem::new("Debug Configuration (Shift+F9)") + PopupMenuItem::new("Settings") .on_click(move |_, _, cx| { - v_debug.update(cx, |_, cx| cx.emit(ToolbarEvent::Debug)); - }), - ) - }) - }) - // Terminal 菜单 - .child({ - let v = view.clone(); - Button::new("menu-terminal") - .small() - .ghost() - .label("Terminal") - .dropdown_menu(move |menu, _window, _cx| { - let v_toggle = v.clone(); - let v_clear = v.clone(); - - menu.item( - PopupMenuItem::new("Toggle Terminal") - .on_click(move |_, _, cx| { - v_toggle.update(cx, |_, cx| { - cx.emit(ToolbarEvent::ToggleTerminal) + v_settings.update(cx, |_, cx| { + cx.emit(ToolbarEvent::OpenSettings) }); }), ) .item( - PopupMenuItem::new("Clear Output") + PopupMenuItem::new("About Lithe") .on_click(move |_, _, cx| { - v_clear.update(cx, |_, cx| { - cx.emit(ToolbarEvent::ClearTerminal) + v_about.update(cx, |_, cx| { + cx.emit(ToolbarEvent::About) }); }), ) - }) - }) - // Help 菜单 - .child({ - let v = view.clone(); - Button::new("menu-help") - .small() - .ghost() - .label("Help") - .dropdown_menu(move |menu, _window, _cx| { - let v_about = v.clone(); - - menu.item( - PopupMenuItem::new("About Lithe") + .separator() + .item( + PopupMenuItem::new("Exit") .on_click(move |_, _, cx| { - v_about.update(cx, |_, cx| cx.emit(ToolbarEvent::About)); + v_exit.update(cx, |_, cx| { + cx.emit(ToolbarEvent::Exit) + }); }), ) }) }) - // 分隔竖线 - .child( - div() - .h(px(14.0)) - .w(px(1.0)) - .bg(ThemeColors::border()) - .mx_2(), - ) - // 项目选择器外观 + // 分支选择器胶囊:带 GitBranch 图标、分支名和 ChevronDown .child( h_flex() .items_center() - .gap_1() + .gap_1p5() .px_2() - .py(px(2.0)) + .py(px(3.0)) .rounded_md() .bg(ThemeColors::bg_tab_hover()) .border_1() .border_color(ThemeColors::border()) - .child("📂") .child( - div() - .text_xs() - .text_color(ThemeColors::text_primary()) - .child(self.workspace_name.clone()), - ), - ) - // Git 分支标识 - .child( - h_flex() - .items_center() - .gap_1() - .px_2() - .py(px(2.0)) - .rounded_md() - .bg(ThemeColors::bg_tab_hover()) - .border_1() - .border_color(ThemeColors::border()) - .child("⎇") + Icon::new(IconName::GitBranch) + .size(px(13.0)) + .text_color(ThemeColors::accent_green()), + ) .child( div() .text_xs() .text_color(ThemeColors::accent_green()) .child(branch), + ) + .child( + Icon::new(IconName::ChevronDown) + .size(px(12.0)) + .text_color(ThemeColors::text_muted()), ), ), ) @@ -307,12 +225,12 @@ impl Render for ToolbarView { // 2. 中间:全局搜索栏 (Search Everywhere) div() .id("search-everywhere-bar") - .w(px(260.0)) - .h(px(26.0)) + .w(px(280.0)) + .h(px(28.0)) .bg(ThemeColors::bg_tab_active()) .border_1() .border_color(ThemeColors::border()) - .rounded_md() + .rounded(px(6.0)) .px_2() .flex() .items_center() @@ -326,7 +244,11 @@ impl Render for ToolbarView { h_flex() .items_center() .gap_2() - .child(div().text_xs().text_color(ThemeColors::text_muted()).child("🔍")) + .child( + Icon::new(IconName::Search) + .size(px(13.0)) + .text_color(ThemeColors::text_muted()), + ) .child( div() .text_xs() @@ -338,22 +260,49 @@ impl Render for ToolbarView { div() .text_xs() .text_color(ThemeColors::text_muted()) - .px_1() + .px_1p5() + .py(px(1.0)) .bg(ThemeColors::bg_titlebar()) - .rounded_sm() + .border_1() + .border_color(ThemeColors::border()) + .rounded(px(4.0)) .child("Ctrl+P"), ), ) .child( - // 3. 右侧:运行/调试配置按钮组 + // 3. 右侧:运行目标胶囊 + 运行/调试/停止/设置控制组 h_flex() .items_center() .gap_2() + .child( + // 运行目标胶囊 + h_flex() + .items_center() + .gap_1p5() + .px_2() + .py(px(3.0)) + .rounded_md() + .bg(ThemeColors::bg_tab_hover()) + .border_1() + .border_color(ThemeColors::border()) + .child( + div() + .text_xs() + .text_color(ThemeColors::text_primary()) + .child("[Project] Default"), + ) + .child( + Icon::new(IconName::ChevronDown) + .size(px(12.0)) + .text_color(ThemeColors::text_muted()), + ), + ) .child( Button::new("tb-run") .small() .primary() - .label("▶ Run") + .icon(IconName::Play) + .label("Run") .on_click(cx.listener(|_this, _event, _window, cx| { cx.emit(ToolbarEvent::Run); })), @@ -362,18 +311,30 @@ impl Render for ToolbarView { Button::new("tb-debug") .small() .ghost() - .label("🐞 Debug") + .icon(IconName::Bug) + .label("Debug") .on_click(cx.listener(|_this, _event, _window, cx| { cx.emit(ToolbarEvent::Debug); })), ) .child( - Button::new("tb-save") + Button::new("tb-stop") + .small() + .ghost() + .icon(IconName::Square) + .tooltip("Stop") + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(ToolbarEvent::Stop); + })), + ) + .child( + Button::new("tb-settings") .small() .ghost() - .label("💾 Save") + .icon(IconName::Settings) + .tooltip("Settings") .on_click(cx.listener(|_this, _event, _window, cx| { - cx.emit(ToolbarEvent::Save); + cx.emit(ToolbarEvent::OpenSettings); })), ), ) diff --git a/linux/src/workbench/view.rs b/linux/src/workbench/view.rs index 044ec4e5a..54ad5f934 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -14,7 +14,7 @@ use crate::workbench::sidebar::{SidebarEvent, SidebarTab, SidebarView}; use crate::workbench::status_bar::StatusBarView; use crate::workbench::toolbar::{ToolbarEvent, ToolbarView}; -/// Linux 前端主工作台视图组件(复刻 Tauri IDE 经典架构) +/// Linux 前端主工作台视图组件(复刻 macOS LitheTheme / Tauri 架构) pub struct WorkbenchView { /// 项目工作区根路径 pub workspace_root: String, @@ -78,7 +78,20 @@ impl WorkbenchView { } }); - // 2. 订阅左侧活动栏 (Activity Rail) 事件 + // 2. 观察侧边栏 Git 分支状态变化并同步至 Toolbar 与 StatusBar + let toolbar_branch_sync = toolbar.clone(); + let status_bar_branch_sync = status_bar.clone(); + let obs_sidebar = cx.observe(&sidebar, move |_this, sidebar, cx| { + let branch = sidebar.read(cx).git_branch.clone(); + let _ = toolbar_branch_sync.update(cx, |tb, cx| { + tb.set_git_branch(branch.clone(), cx); + }); + let _ = status_bar_branch_sync.update(cx, |sb, cx| { + sb.set_git_branch(branch, cx); + }); + }); + + // 3. 订阅左侧活动栏 (Activity Rail) 事件 let sidebar_clone = sidebar.clone(); let bottom_panel_clone = bottom_panel.clone(); let sub_rail = cx.subscribe(&activity_rail, move |this, rail, event: &ActivityRailEvent, cx| { @@ -116,7 +129,7 @@ impl WorkbenchView { } }); - // 3. 订阅顶部标题栏与菜单栏事件 + // 4. 订阅顶部工具栏事件 let sub_toolbar = cx.subscribe(&toolbar, |this, _toolbar, event: &ToolbarEvent, cx| { match event { ToolbarEvent::NewFile => { @@ -192,9 +205,14 @@ impl WorkbenchView { bp.append_log("[Stop] Session terminated by user.".to_string(), cx); }); } + ToolbarEvent::OpenSettings => { + let _ = this.bottom_panel.update(cx, |bp, cx| { + bp.append_log("[Settings] Settings dialog requested".to_string(), cx); + }); + } ToolbarEvent::About => { let _ = this.bottom_panel.update(cx, |bp, cx| { - bp.append_log("[About] Lithe IDE for Linux (Powered by Zed GPUI Kit & Rust Core)".to_string(), cx); + bp.append_log("[About] Lithe IDE for Linux (Powered by GPUI Kit & Rust Core)".to_string(), cx); }); } ToolbarEvent::Exit => { @@ -213,7 +231,7 @@ impl WorkbenchView { bottom_panel, status_bar, client: CoreClient::new(), - _subscriptions: vec![sub_sidebar, sub_rail, sub_toolbar], + _subscriptions: vec![sub_sidebar, obs_sidebar, sub_rail, sub_toolbar], } } @@ -251,7 +269,7 @@ impl Render for WorkbenchView { .size_full() .bg(ThemeColors::bg_editor()) .child( - // 1. 顶部标题栏/菜单栏/运行控制 (Title Bar) + // 1. 顶部标题栏/工具栏 (Toolbar) self.toolbar.clone(), ) .child( From 8c334ec238a48a4b5f586424902adf943d7d236a Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 11:02:41 +0800 Subject: [PATCH 04/59] =?UTF-8?q?feat(linux):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=99=A8=E6=BB=9A=E5=8A=A8=E4=B8=8E=E9=80=89?= =?UTF-8?q?=E4=B8=AD=EF=BC=8C=E8=A1=A5=E9=BD=90=E4=BE=A7=E8=BE=B9=E6=A0=8F?= =?UTF-8?q?/=E6=90=9C=E7=B4=A2/=E8=AE=BE=E7=BD=AE=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - editor: 正文改用 gpui-kit 上游 Editor/EditorState,恢复上下滚动、 文本选中与复制;标签内容与语言在切换时同步进编辑器,编辑变更回写 标签并置脏,保存读取编辑器实时文本。 - view: 接入 SearchEverywhere 与 SettingsDialog 事件;支持侧边栏宽度、 底部面板高度的拖拽调整(带边界 clamp)。 - sidebar: 扩展 SidebarEvent/SidebarTab 与文件树交互。 - bottom_panel: 高度可配置。 - 忽略 .codegraph/ 索引目录。 --- .gitignore | 1 + linux/src/workbench/bottom_panel.rs | 9 +- linux/src/workbench/editor.rs | 295 ++++++---- linux/src/workbench/mod.rs | 10 +- linux/src/workbench/search_everywhere.rs | 613 ++++++++++++++++++++ linux/src/workbench/settings_dialog.rs | 694 +++++++++++++++++++++++ linux/src/workbench/sidebar.rs | 245 +++++--- linux/src/workbench/view.rs | 471 +++++++++++++-- 8 files changed, 2081 insertions(+), 257 deletions(-) create mode 100644 linux/src/workbench/search_everywhere.rs create mode 100644 linux/src/workbench/settings_dialog.rs diff --git a/.gitignore b/.gitignore index 38786b01b..82d0575f5 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ docs/visual-qa/ .reasonix/ reasonix.toml .codex/ +.codegraph/ /node_modules/ *.swp *.swo diff --git a/linux/src/workbench/bottom_panel.rs b/linux/src/workbench/bottom_panel.rs index 711d70dc9..8d90d7e4a 100644 --- a/linux/src/workbench/bottom_panel.rs +++ b/linux/src/workbench/bottom_panel.rs @@ -21,6 +21,7 @@ pub enum BottomTab { pub struct BottomPanelView { pub active_tab: BottomTab, pub is_collapsed: bool, + pub height: f32, pub terminal: Entity, pub output_logs: Vec, pub problems: Vec, @@ -33,12 +34,18 @@ impl BottomPanelView { Self { active_tab: BottomTab::Terminal, is_collapsed: false, + height: 240.0, terminal, output_logs: vec!["[Lithe Linux] Initialized.".to_string()], problems: Vec::new(), } } + 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; @@ -148,7 +155,7 @@ impl Render for BottomPanelView { } v_flex() - .h(px(240.0)) + .h(px(self.height)) .w_full() .bg(ThemeColors::bg_bottom_panel()) .border_t_1() diff --git a/linux/src/workbench/editor.rs b/linux/src/workbench/editor.rs index 5049f875f..cd505bc84 100644 --- a/linux/src/workbench/editor.rs +++ b/linux/src/workbench/editor.rs @@ -1,10 +1,12 @@ use gpui_kit::assets::IconName; +use gpui_kit::component::input::{Editor, EditorState, InputEvent}; 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, Context, FontWeight, InteractiveElement as _, IntoElement, ParentElement as _, - Render, StatefulInteractiveElement as _, Styled as _, Window, + div, px, relative, AppContext as _, Context, Entity, FontWeight, InteractiveElement as _, + IntoElement, ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, + Subscription, Window, }; use crate::core::CoreClient; @@ -23,22 +25,45 @@ pub struct EditorTab { } /// 多标签代码编辑器组件(对齐 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, + /// 已同步到 `editor_state` 的标签索引。 + synced_tab: Option, + /// 活动标签内容被外部替换(重新打开文件)时置位,渲染时重新灌入编辑器。 + sync_needed: bool, + /// 监听编辑器文本变化以回写标签内容与脏标记。 + _editor_subscription: Subscription, client: CoreClient, } impl EditorView { - pub fn new(workspace_root: String, _cx: &mut Context) -> Self { + pub fn new(workspace_root: String, window: &mut Window, cx: &mut Context) -> Self { + let editor_state = cx.new(|cx| EditorState::new(window, cx).line_number(true)); + 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, + synced_tab: None, + sync_needed: false, + _editor_subscription, client: CoreClient::new(), } } @@ -46,6 +71,13 @@ impl EditorView { /// 打开文件,如果已在标签中则切换,否则新增标签 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); cx.notify(); return; @@ -66,6 +98,7 @@ impl EditorView { }); self.active_tab_index = Some(self.tabs.len() - 1); + self.sync_needed = true; cx.notify(); } @@ -78,12 +111,60 @@ impl EditorView { } 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(); + 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; + } + + /// 编辑器文本变化后回写标签内容并标记为已修改。 + 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; + tab.is_dirty = true; + } + } + cx.notify(); + } + /// 保存当前活动的标签页至磁盘(对接 `file.write`) pub fn save_active(&mut self, cx: &mut Context) { let Some(idx) = self.active_tab_index else { @@ -93,9 +174,11 @@ impl EditorView { return; }; + // 以编辑器内的实时文本为准,避免依赖事件回写的时序。 + let text = self.editor_state.read(cx).value().to_string(); + let root = self.workspace_root.clone(); let path = tab.path.clone(); - let text = tab.content.clone(); let client = self.client.clone(); cx.spawn(async move |this, cx| { @@ -173,11 +256,15 @@ fn is_code_file(name: &str) -> bool { } impl Render for EditorView { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + self.sync_active_editor(window, cx); + let active_tab = self .active_tab_index .and_then(|idx| self.tabs.get(idx).cloned()); + let editor_state = self.editor_state.clone(); + v_flex() .size_full() .bg(ThemeColors::bg_editor()) @@ -328,118 +415,94 @@ impl Render for EditorView { ) }) .child( - // 3. 中央代码内容区 - div().flex_1().w_full().overflow_scrollbar().child( - if let Some(tab) = &active_tab { - let lines: Vec = tab.content.lines().map(|s| s.to_string()).collect(); - let total_lines = lines.len().max(1); - - h_flex() - .size_full() - .p_2() - .font_family("monospace") - .text_xs() - .child( - // 代码行号槽(Gutter) - v_flex() - .flex_shrink_0() - .w(px(48.0)) - .pr_3() - .border_r_1() - .border_color(ThemeColors::border()) - .text_color(ThemeColors::text_muted()) - .items_end() - .children((1..=total_lines).map(|num| { - div().h(px(20.0)).child(format!("{num}")) - })), - ) - .child( - // 代码文本展示行 - v_flex() - .flex_1() - .pl_3() - .text_color(ThemeColors::text_primary()) - .children(lines.into_iter().enumerate().map(|(idx, line)| { - div() - .id(idx) - .h(px(20.0)) - .child(if line.is_empty() { " ".to_string() } else { line }) - })), - ) - } else { + // 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), + ) + }) + .when(active_tab.is_none(), |this| { // 无打开文件 Empty State(精致 Lithe 居中徽标与操作提示) - h_flex() - .size_full() - .items_center() - .justify_center() - .child( - v_flex() - .items_center() - .gap_3() - .child( - Icon::new(IconName::Zap) - .size(px(48.0)) - .text_color(ThemeColors::accent_blue()), - ) - .child( - div() - .text_lg() - .font_weight(FontWeight::BOLD) - .text_color(ThemeColors::text_primary()) - .child("Lithe IDEA"), - ) - .child( - div() - .text_xs() - .text_color(ThemeColors::text_muted()) - .child("Next-generation IDE for Linux, powered by GPUI Kit & Rust Core"), - ) - .child( - v_flex() - .pt_2() - .gap_2() - .child( - h_flex() - .items_center() - .gap_2() - .text_xs() - .text_color(ThemeColors::text_muted()) - .child("Search files everywhere:") - .child( - div() - .px_1p5() - .py(px(1.0)) - .bg(ThemeColors::bg_tab_hover()) - .border_1() - .border_color(ThemeColors::border()) - .rounded_sm() - .text_color(ThemeColors::text_primary()) - .child("Ctrl+P"), - ), - ) - .child( - h_flex() - .items_center() - .gap_2() - .text_xs() - .text_color(ThemeColors::text_muted()) - .child("Toggle terminal panel:") - .child( - div() - .px_1p5() - .py(px(1.0)) - .bg(ThemeColors::bg_tab_hover()) - .border_1() - .border_color(ThemeColors::border()) - .rounded_sm() - .text_color(ThemeColors::text_primary()) - .child("Ctrl+`"), - ), - ), - ), - ) - }, - ), + this.child( + h_flex() + .size_full() + .items_center() + .justify_center() + .child( + v_flex() + .items_center() + .gap_3() + .child( + Icon::new(IconName::Zap) + .size(px(48.0)) + .text_color(ThemeColors::accent_blue()), + ) + .child( + div() + .text_lg() + .font_weight(FontWeight::BOLD) + .text_color(ThemeColors::text_primary()) + .child("Lithe IDEA"), + ) + .child( + div() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child("Next-generation IDE for Linux, powered by GPUI Kit & Rust Core"), + ) + .child( + v_flex() + .pt_2() + .gap_2() + .child( + h_flex() + .items_center() + .gap_2() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child("Search files everywhere:") + .child( + div() + .px_1p5() + .py(px(1.0)) + .bg(ThemeColors::bg_tab_hover()) + .border_1() + .border_color(ThemeColors::border()) + .rounded_sm() + .text_color(ThemeColors::text_primary()) + .child("Ctrl+P"), + ), + ) + .child( + h_flex() + .items_center() + .gap_2() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child("Toggle terminal panel:") + .child( + div() + .px_1p5() + .py(px(1.0)) + .bg(ThemeColors::bg_tab_hover()) + .border_1() + .border_color(ThemeColors::border()) + .rounded_sm() + .text_color(ThemeColors::text_primary()) + .child("Ctrl+`"), + ), + ), + ), + ), + ) + }), ) } } diff --git a/linux/src/workbench/mod.rs b/linux/src/workbench/mod.rs index 5a3e7857f..e397bbc73 100644 --- a/linux/src/workbench/mod.rs +++ b/linux/src/workbench/mod.rs @@ -1,6 +1,8 @@ pub mod activity_rail; pub mod bottom_panel; pub mod editor; +pub mod search_everywhere; +pub mod settings_dialog; pub mod sidebar; pub mod status_bar; pub mod terminal; @@ -14,11 +16,15 @@ pub use bottom_panel::BottomPanelView; #[allow(unused_imports)] pub use editor::EditorView; #[allow(unused_imports)] -pub use sidebar::SidebarView; +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::StatusBarView; #[allow(unused_imports)] pub use terminal::TerminalView; #[allow(unused_imports)] -pub use toolbar::ToolbarView; +pub use toolbar::{ToolbarEvent, ToolbarView}; pub use view::WorkbenchView; diff --git a/linux/src/workbench/search_everywhere.rs b/linux/src/workbench/search_everywhere.rs new file mode 100644 index 000000000..4ce398b58 --- /dev/null +++ b/linux/src/workbench/search_everywhere.rs @@ -0,0 +1,613 @@ +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; + +#[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, + name: 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) -> 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 + if self.scope == SearchScope::All || self.scope == SearchScope::Actions { + for act in &self.actions { + if q.is_empty() + || act.name.to_lowercase().contains(&q) + || act.id.to_lowercase().contains(&q) + { + results.push(MatchedItem::Action { + id: act.id.clone(), + name: act.name.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(); + 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().len(); + if total > 0 && this.selected_index + 1 < total { + this.selected_index += 1; + cx.notify(); + } + } + "enter" => { + let items = this.filtered_items(); + 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| { + // 阻止向外层遮罩冒泡关闭 + }), + ) + .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() { + "Search files, actions, symbols (Type to search)..." + .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", + "All", + self.scope == SearchScope::All, + SearchScope::All, + cx, + )) + .child(self.render_scope_pill( + "scope-files", + "Files", + self.scope == SearchScope::Files, + SearchScope::Files, + cx, + )) + .child(self.render_scope_pill( + "scope-actions", + "Actions", + 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("No matching files or actions found"), + ) + }) + .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 { + name, + 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(name.clone()), + ) + .into_any_element(), + }) + .child(match &item { + MatchedItem::File { .. } => div() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child("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_between() + .px_3() + .bg(ThemeColors::bg_titlebar()) + .border_t_1() + .border_color(ThemeColors::border()) + .child( + div() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child("↑↓ Navigate · Enter Select · Esc Close"), + ) + .child( + div() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child("Search Everywhere"), + ), + ), + ) + } +} + +impl SearchEverywhereModal { + fn render_scope_pill( + &self, + id: &'static str, + label: &'static str, + 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..14dde8f32 --- /dev/null +++ b/linux/src/workbench/settings_dialog.rs @@ -0,0 +1,694 @@ +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, FontWeight, InteractiveElement as _, IntoElement, + ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Window, +}; + +use crate::theme::ThemeColors; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SettingsCategory { + General, + Editor, + Git, + Terminal, +} + +#[derive(Debug, Clone)] +pub enum SettingsEvent { + Close, +} + +/// 居中设置面板(宽 640px,高 440px) +pub struct SettingsDialog { + pub active_category: SettingsCategory, + // General 设置 + pub theme_name: String, + pub ui_scale: String, + pub language: String, + // Editor 设置 + pub font_size: usize, + pub tab_size: usize, + pub show_line_numbers: bool, + pub word_wrap: bool, + // Git 设置 + pub git_auto_fetch: bool, + pub git_format_on_commit: bool, + pub git_gpg_sign: bool, + // Terminal 设置 + pub default_shell: String, + pub scrollback_lines: usize, +} + +impl EventEmitter for SettingsDialog {} + +impl SettingsDialog { + pub fn new() -> Self { + Self { + active_category: SettingsCategory::General, + theme_name: "Dark Default".to_string(), + ui_scale: "100%".to_string(), + language: "English".to_string(), + font_size: 14, + tab_size: 4, + show_line_numbers: true, + word_wrap: false, + git_auto_fetch: true, + git_format_on_commit: true, + git_gpg_sign: false, + default_shell: std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string()), + scrollback_lines: 1000, + } + } + + pub fn set_category(&mut self, cat: SettingsCategory, cx: &mut Context) { + self.active_category = cat; + cx.notify(); + } +} + +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( + // 居中模态面板:宽 640px,高 440px + v_flex() + .id("settings-dialog-card") + .w(px(640.0)) + .h(px(440.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| { + // 阻止向遮罩冒泡 + }), + ) + .child( + // 1. 头部标题栏:带 IconName::Settings 标题与 IconName::Close 关闭按钮 + h_flex() + .h(px(42.0)) + .w_full() + .items_center() + .justify_between() + .px_4() + .border_b_1() + .border_color(ThemeColors::border()) + .bg(ThemeColors::bg_titlebar()) + .child( + h_flex() + .items_center() + .gap_2() + .child( + Icon::new(IconName::Settings) + .size(px(16.0)) + .text_color(ThemeColors::accent_blue()), + ) + .child( + div() + .text_sm() + .font_weight(FontWeight::BOLD) + .text_color(ThemeColors::text_primary()) + .child("Settings"), + ), + ) + .child( + Button::new("close-settings-btn") + .small() + .ghost() + .icon(IconName::Close) + .tooltip("Close") + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(SettingsEvent::Close); + })), + ), + ) + .child( + // 2. 主体区(左侧分类导航 + 右侧设置内容) + h_flex() + .flex_1() + .w_full() + .child( + // 左侧分类导航 + v_flex() + .w(px(160.0)) + .h_full() + .bg(ThemeColors::bg_sidebar()) + .border_r_1() + .border_color(ThemeColors::border()) + .py_2() + .gap_1() + .child(self.render_nav_item( + "nav-general", + IconName::SlidersHorizontal, + "General", + self.active_category == SettingsCategory::General, + SettingsCategory::General, + cx, + )) + .child(self.render_nav_item( + "nav-editor", + IconName::FileCode, + "Editor", + self.active_category == SettingsCategory::Editor, + SettingsCategory::Editor, + cx, + )) + .child(self.render_nav_item( + "nav-git", + IconName::GitBranch, + "Git", + self.active_category == SettingsCategory::Git, + SettingsCategory::Git, + cx, + )) + .child(self.render_nav_item( + "nav-terminal", + IconName::Terminal, + "Terminal", + self.active_category == SettingsCategory::Terminal, + SettingsCategory::Terminal, + cx, + )), + ) + .child( + // 右侧设置内容详情 + div() + .flex_1() + .h_full() + .bg(ThemeColors::bg_editor()) + .p_4() + .overflow_y_scrollbar() + .child(match self.active_category { + SettingsCategory::General => { + self.render_general_settings(cx).into_any_element() + } + SettingsCategory::Editor => { + self.render_editor_settings(cx).into_any_element() + } + SettingsCategory::Git => { + self.render_git_settings(cx).into_any_element() + } + SettingsCategory::Terminal => { + self.render_terminal_settings(cx).into_any_element() + } + }), + ), + ), + ) + } +} + +impl SettingsDialog { + fn render_nav_item( + &self, + id: &'static str, + icon: IconName, + label: &'static str, + is_active: bool, + cat: SettingsCategory, + cx: &mut Context, + ) -> impl IntoElement { + h_flex() + .id(id) + .h(px(32.0)) + .w_full() + .items_center() + .gap_2p5() + .px_3() + .cursor_pointer() + .text_xs() + .when(is_active, |row| { + row.bg(ThemeColors::subtle_selection()) + .border_l_2() + .border_color(ThemeColors::accent_blue()) + .text_color(ThemeColors::text_primary()) + .font_weight(FontWeight::BOLD) + }) + .when(!is_active, |row| { + row.text_color(ThemeColors::text_muted()).hover(|h| { + h.bg(ThemeColors::bg_tab_hover()) + .text_color(ThemeColors::text_primary()) + }) + }) + .child( + Icon::new(icon) + .size(px(14.0)) + .text_color(if is_active { + ThemeColors::accent_blue() + } else { + ThemeColors::text_muted() + }), + ) + .child(label) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.set_category(cat, cx); + })) + } + + fn render_general_settings(&self, cx: &mut Context) -> impl IntoElement { + v_flex() + .size_full() + .gap_4() + .child(self.render_section_title("Appearance & General")) + // 外观主题 + .child(self.render_setting_row( + "Theme", + "Color theme for editor and workbench interface", + h_flex() + .gap_1p5() + .child(self.render_choice_pill( + "theme-dark", + "Dark Default", + self.theme_name == "Dark Default", + cx.listener(|this, _event, _window, cx| { + this.theme_name = "Dark Default".to_string(); + cx.notify(); + }), + )) + .child(self.render_choice_pill( + "theme-contrast", + "High Contrast", + self.theme_name == "High Contrast", + cx.listener(|this, _event, _window, cx| { + this.theme_name = "High Contrast".to_string(); + cx.notify(); + }), + )), + )) + // 界面缩放 + .child(self.render_setting_row( + "UI Scale", + "Display scaling factor for the entire workspace", + h_flex() + .gap_1p5() + .child(self.render_choice_pill( + "scale-100", + "100%", + self.ui_scale == "100%", + cx.listener(|this, _event, _window, cx| { + this.ui_scale = "100%".to_string(); + cx.notify(); + }), + )) + .child(self.render_choice_pill( + "scale-125", + "125%", + self.ui_scale == "125%", + cx.listener(|this, _event, _window, cx| { + this.ui_scale = "125%".to_string(); + cx.notify(); + }), + )), + )) + // 语言 + .child(self.render_setting_row( + "Language", + "Application display and localization language", + h_flex() + .gap_1p5() + .child(self.render_choice_pill( + "lang-en", + "English", + self.language == "English", + cx.listener(|this, _event, _window, cx| { + this.language = "English".to_string(); + cx.notify(); + }), + )) + .child(self.render_choice_pill( + "lang-zh", + "简体中文", + self.language == "简体中文", + cx.listener(|this, _event, _window, cx| { + this.language = "简体中文".to_string(); + cx.notify(); + }), + )), + )) + } + + fn render_editor_settings(&self, cx: &mut Context) -> impl IntoElement { + v_flex() + .size_full() + .gap_4() + .child(self.render_section_title("Editor Settings")) + // 字体大小 + .child(self.render_setting_row( + "Font Size", + "Controls editor text font size in pixels", + h_flex() + .gap_1p5() + .child(self.render_choice_pill( + "font-13", + "13px", + self.font_size == 13, + cx.listener(|this, _event, _window, cx| { + this.font_size = 13; + cx.notify(); + }), + )) + .child(self.render_choice_pill( + "font-14", + "14px", + self.font_size == 14, + cx.listener(|this, _event, _window, cx| { + this.font_size = 14; + cx.notify(); + }), + )) + .child(self.render_choice_pill( + "font-16", + "16px", + self.font_size == 16, + cx.listener(|this, _event, _window, cx| { + this.font_size = 16; + cx.notify(); + }), + )), + )) + // Tab 缩进 + .child(self.render_setting_row( + "Tab Size", + "Number of spaces per indentation level", + h_flex() + .gap_1p5() + .child(self.render_choice_pill( + "tab-2", + "2 Spaces", + self.tab_size == 2, + cx.listener(|this, _event, _window, cx| { + this.tab_size = 2; + cx.notify(); + }), + )) + .child(self.render_choice_pill( + "tab-4", + "4 Spaces", + self.tab_size == 4, + cx.listener(|this, _event, _window, cx| { + this.tab_size = 4; + cx.notify(); + }), + )), + )) + // 显示行号 + .child(self.render_setting_row( + "Line Numbers", + "Display line number gutter in code editor", + h_flex() + .gap_1p5() + .child(self.render_choice_pill( + "ln-show", + "Show", + self.show_line_numbers, + cx.listener(|this, _event, _window, cx| { + this.show_line_numbers = true; + cx.notify(); + }), + )) + .child(self.render_choice_pill( + "ln-hide", + "Hide", + !self.show_line_numbers, + cx.listener(|this, _event, _window, cx| { + this.show_line_numbers = false; + cx.notify(); + }), + )), + )) + // 自动换行 + .child(self.render_setting_row( + "Word Wrap", + "Wrap long lines to fit the editor viewport width", + h_flex() + .gap_1p5() + .child(self.render_choice_pill( + "wrap-on", + "On", + self.word_wrap, + cx.listener(|this, _event, _window, cx| { + this.word_wrap = true; + cx.notify(); + }), + )) + .child(self.render_choice_pill( + "wrap-off", + "Off", + !self.word_wrap, + cx.listener(|this, _event, _window, cx| { + this.word_wrap = false; + cx.notify(); + }), + )), + )) + } + + fn render_git_settings(&self, cx: &mut Context) -> impl IntoElement { + v_flex() + .size_full() + .gap_4() + .child(self.render_section_title("Version Control & Git")) + // 自动拉取 + .child(self.render_setting_row( + "Auto Fetch", + "Automatically fetch remote branch status periodically", + h_flex() + .gap_1p5() + .child(self.render_choice_pill( + "fetch-on", + "Enabled", + self.git_auto_fetch, + cx.listener(|this, _event, _window, cx| { + this.git_auto_fetch = true; + cx.notify(); + }), + )) + .child(self.render_choice_pill( + "fetch-off", + "Disabled", + !self.git_auto_fetch, + cx.listener(|this, _event, _window, cx| { + this.git_auto_fetch = false; + cx.notify(); + }), + )), + )) + // Commit 前格式化 + .child(self.render_setting_row( + "Format on Commit", + "Run code formatter on staged changes before committing", + h_flex() + .gap_1p5() + .child(self.render_choice_pill( + "format-on", + "Enabled", + self.git_format_on_commit, + cx.listener(|this, _event, _window, cx| { + this.git_format_on_commit = true; + cx.notify(); + }), + )) + .child(self.render_choice_pill( + "format-off", + "Disabled", + !self.git_format_on_commit, + cx.listener(|this, _event, _window, cx| { + this.git_format_on_commit = false; + cx.notify(); + }), + )), + )) + // GPG 签名 + .child(self.render_setting_row( + "GPG Commit Signing", + "Sign commits with user default GPG signing key", + h_flex() + .gap_1p5() + .child(self.render_choice_pill( + "gpg-on", + "Enabled", + self.git_gpg_sign, + cx.listener(|this, _event, _window, cx| { + this.git_gpg_sign = true; + cx.notify(); + }), + )) + .child(self.render_choice_pill( + "gpg-off", + "Disabled", + !self.git_gpg_sign, + cx.listener(|this, _event, _window, cx| { + this.git_gpg_sign = false; + cx.notify(); + }), + )), + )) + } + + fn render_terminal_settings(&self, cx: &mut Context) -> impl IntoElement { + v_flex() + .size_full() + .gap_4() + .child(self.render_section_title("Terminal & PTY")) + // 默认终端 + .child(self.render_setting_row( + "Default Shell", + "System shell binary used for bottom terminal panel", + h_flex() + .gap_1p5() + .child(self.render_choice_pill( + "shell-bash", + "/bin/bash", + self.default_shell.contains("bash"), + cx.listener(|this, _event, _window, cx| { + this.default_shell = "/bin/bash".to_string(); + cx.notify(); + }), + )) + .child(self.render_choice_pill( + "shell-zsh", + "/bin/zsh", + self.default_shell.contains("zsh"), + cx.listener(|this, _event, _window, cx| { + this.default_shell = "/bin/zsh".to_string(); + cx.notify(); + }), + )) + .child(self.render_choice_pill( + "shell-sh", + "/bin/sh", + self.default_shell == "/bin/sh", + cx.listener(|this, _event, _window, cx| { + this.default_shell = "/bin/sh".to_string(); + cx.notify(); + }), + )), + )) + // 滚动缓冲区大小 + .child(self.render_setting_row( + "Scrollback Buffer", + "Maximum number of terminal output lines kept in memory", + h_flex() + .gap_1p5() + .child(self.render_choice_pill( + "scroll-1000", + "1000 lines", + self.scrollback_lines == 1000, + cx.listener(|this, _event, _window, cx| { + this.scrollback_lines = 1000; + cx.notify(); + }), + )) + .child(self.render_choice_pill( + "scroll-5000", + "5000 lines", + self.scrollback_lines == 5000, + cx.listener(|this, _event, _window, cx| { + this.scrollback_lines = 5000; + cx.notify(); + }), + )), + )) + } + + fn render_section_title(&self, title: &'static str) -> impl IntoElement { + div() + .text_sm() + .font_weight(FontWeight::BOLD) + .text_color(ThemeColors::text_primary()) + .pb_1() + .border_b_1() + .border_color(ThemeColors::border()) + .child(title) + } + + fn render_setting_row( + &self, + label: &'static str, + desc: &'static str, + control: impl IntoElement, + ) -> impl IntoElement { + h_flex() + .w_full() + .items_center() + .justify_between() + .gap_4() + .child( + v_flex() + .flex_1() + .gap_0p5() + .child( + div() + .text_xs() + .font_weight(FontWeight::MEDIUM) + .text_color(ThemeColors::text_primary()) + .child(label), + ) + .child( + div() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(desc), + ), + ) + .child(control) + } + + fn render_choice_pill( + &self, + id: &'static str, + label: &'static str, + is_selected: bool, + click_handler: impl Fn(&gpui_kit::ClickEvent, &mut Window, &mut gpui_kit::App) + 'static, + ) -> impl IntoElement { + h_flex() + .id(id) + .items_center() + .px_2p5() + .py(px(3.0)) + .rounded(px(4.0)) + .border_1() + .cursor_pointer() + .text_xs() + .when(is_selected, |pill| { + pill.bg(ThemeColors::accent_blue()) + .border_color(ThemeColors::accent_blue()) + .text_color(ThemeColors::text_primary()) + .font_weight(FontWeight::BOLD) + }) + .when(!is_selected, |pill| { + pill.bg(ThemeColors::bg_tab_bar()) + .border_color(ThemeColors::border()) + .text_color(ThemeColors::text_muted()) + .hover(|h| { + h.bg(ThemeColors::bg_tab_hover()) + .text_color(ThemeColors::text_primary()) + }) + }) + .child(label) + .on_click(click_handler) + } +} diff --git a/linux/src/workbench/sidebar.rs b/linux/src/workbench/sidebar.rs index b69af9ac1..dd285c192 100644 --- a/linux/src/workbench/sidebar.rs +++ b/linux/src/workbench/sidebar.rs @@ -78,6 +78,8 @@ impl FileEntry { #[derive(Debug, Clone)] pub enum SidebarEvent { OpenFile(String), + NewFile, + Commit(String), } #[derive(Debug, Clone)] @@ -113,6 +115,7 @@ pub struct SidebarView { pub git_branch: Option, pub git_changes: Vec, pub is_git_loading: bool, + pub git_commit_message: String, client: CoreClient, } @@ -133,6 +136,7 @@ impl SidebarView { git_branch: None, git_changes: Vec::new(), is_git_loading: false, + git_commit_message: String::new(), client: CoreClient::new(), }; @@ -371,7 +375,7 @@ impl Render for SidebarView { .border_r_1() .border_color(ThemeColors::border()) .child( - // 顶部工具窗口精简标题 + 刷新操作按钮(无老旧横排Tab栏) + // 顶部工具窗口精简标题 + 刷新/新建文件操作按钮(无老旧横排Tab栏) h_flex() .h(px(32.0)) .w_full() @@ -389,15 +393,32 @@ impl Render for SidebarView { .child(title), ) .child( - Button::new("sidebar-refresh") - .small() - .ghost() - .icon(IconName::RotateCw) - .tooltip("Refresh") - .on_click(cx.listener(|this, _event, _window, cx| { - this.refresh(cx); - this.refresh_git(cx); - })), + h_flex() + .items_center() + .gap_1() + .when(self.active_tab == SidebarTab::Explorer, |buttons| { + buttons.child( + Button::new("sidebar-new-file") + .small() + .ghost() + .icon(IconName::FilePlus) + .tooltip("New File") + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(SidebarEvent::NewFile); + })), + ) + }) + .child( + Button::new("sidebar-refresh") + .small() + .ghost() + .icon(IconName::RotateCw) + .tooltip("Refresh") + .on_click(cx.listener(|this, _event, _window, cx| { + this.refresh(cx); + this.refresh_git(cx); + })), + ), ), ) .child( @@ -631,91 +652,157 @@ impl SidebarView { v_flex() .size_full() - .p_2() - .gap_2() + .justify_between() .child( - h_flex() - .items_center() + // 1. Git 变更文件列表区 + v_flex() + .flex_1() + .w_full() + .p_2() .gap_2() - .pb_2() - .border_b_1() - .border_color(ThemeColors::border()) + .overflow_y_scrollbar() .child( - Icon::new(IconName::GitBranch) - .size(px(13.0)) - .text_color(ThemeColors::accent_green()), + h_flex() + .items_center() + .gap_2() + .pb_2() + .border_b_1() + .border_color(ThemeColors::border()) + .child( + Icon::new(IconName::GitBranch) + .size(px(13.0)) + .text_color(ThemeColors::accent_green()), + ) + .child( + div() + .text_xs() + .font_weight(FontWeight::BOLD) + .text_color(ThemeColors::accent_green()) + .child(branch), + ), ) .child( div() .text_xs() - .font_weight(FontWeight::BOLD) - .text_color(ThemeColors::accent_green()) - .child(branch), - ), - ) - .child( - div() - .text_xs() - .text_color(ThemeColors::text_muted()) - .child(format!("Changed Files ({})", self.git_changes.len())), - ) - .children(self.git_changes.iter().enumerate().map(|(idx, change)| { - let path = change.path.clone(); - let status = change.status.clone(); - let status_color = match status.as_str() { - "M" => ThemeColors::accent_yellow(), - "A" => ThemeColors::accent_green(), - "D" => ThemeColors::accent_red(), - _ => ThemeColors::text_muted(), - }; - - let icon_name = if is_code_file(&path) { - IconName::FileCode - } else { - IconName::FileText - }; + .text_color(ThemeColors::text_muted()) + .child(format!("Changed Files ({})", self.git_changes.len())), + ) + .children(self.git_changes.iter().enumerate().map(|(idx, change)| { + let path = change.path.clone(); + let status = change.status.clone(); + let status_color = match status.as_str() { + "M" => ThemeColors::accent_yellow(), + "A" => ThemeColors::accent_green(), + "D" => ThemeColors::accent_red(), + _ => ThemeColors::text_muted(), + }; + + let icon_name = if is_code_file(&path) { + IconName::FileCode + } else { + IconName::FileText + }; - h_flex() - .id(idx) - .h(px(24.0)) - .w_full() - .items_center() - .justify_between() - .px_2() - .rounded_sm() - .cursor_pointer() - .hover(|h| h.bg(ThemeColors::bg_tab_hover())) - .text_xs() - .child( h_flex() + .id(idx) + .h(px(24.0)) + .w_full() .items_center() - .gap_2() + .justify_between() + .px_2() + .rounded_sm() + .cursor_pointer() + .hover(|h| h.bg(ThemeColors::bg_tab_hover())) + .text_xs() .child( - Icon::new(icon_name) - .size(px(13.0)) - .text_color(ThemeColors::text_muted()), + h_flex() + .items_center() + .gap_2() + .child( + Icon::new(icon_name) + .size(px(13.0)) + .text_color(ThemeColors::text_muted()), + ) + .child( + div() + .text_color(ThemeColors::text_primary()) + .truncate() + .child(path.clone()), + ), ) .child( div() - .text_color(ThemeColors::text_primary()) - .truncate() - .child(path.clone()), - ), - ) + .font_weight(FontWeight::BOLD) + .text_color(status_color) + .child(status), + ) + .on_click(cx.listener({ + let p = path.clone(); + move |this, _event, _window, cx| { + this.selected_path = Some(p.clone()); + cx.emit(SidebarEvent::OpenFile(p.clone())); + cx.notify(); + } + })) + })), + ) + .child( + // 2. Git 面板底部 Commit 工作区 + v_flex() + .w_full() + .p_2() + .border_t_1() + .border_color(ThemeColors::border()) + .bg(ThemeColors::bg_tab_bar()) + .gap_2() .child( + // Commit message 输入提示卡片 div() - .font_weight(FontWeight::BOLD) - .text_color(status_color) - .child(status), + .w_full() + .bg(ThemeColors::bg_editor()) + .border_1() + .border_color(ThemeColors::border()) + .rounded(px(4.0)) + .p_2() + .text_xs() + .text_color(if self.git_commit_message.is_empty() { + ThemeColors::text_muted() + } else { + ThemeColors::text_primary() + }) + .child(if self.git_commit_message.is_empty() { + "Commit message (e.g. feat: update files)...".to_string() + } else { + self.git_commit_message.clone() + }), ) - .on_click(cx.listener({ - let p = path.clone(); - move |this, _event, _window, cx| { - this.selected_path = Some(p.clone()); - cx.emit(SidebarEvent::OpenFile(p.clone())); - cx.notify(); - } - })) - })) + .child( + h_flex() + .w_full() + .items_center() + .justify_between() + .child( + div() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(format!("{} changed", self.git_changes.len())), + ) + .child( + Button::new("git-commit-btn") + .small() + .primary() + .icon(IconName::Check) + .label("Commit") + .on_click(cx.listener(|this, _event, _window, cx| { + let msg = if this.git_commit_message.trim().is_empty() { + "feat: update workspace files".to_string() + } else { + this.git_commit_message.trim().to_string() + }; + cx.emit(SidebarEvent::Commit(msg)); + })), + ), + ), + ) } } diff --git a/linux/src/workbench/view.rs b/linux/src/workbench/view.rs index 54ad5f934..2a7b2950c 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -1,8 +1,9 @@ use gpui_kit::component::{h_flex, v_flex}; use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::{ - div, px, AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, - Styled as _, Subscription, Window, + div, px, AppContext as _, Context, Entity, FocusHandle, InteractiveElement as _, IntoElement, + KeyDownEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, + Render, Styled as _, Subscription, Window, }; use crate::core::CoreClient; @@ -10,16 +11,37 @@ use crate::theme::ThemeColors; use crate::workbench::activity_rail::{ActivityRailEvent, ActivityRailView, ActivityTab}; use crate::workbench::bottom_panel::BottomPanelView; use crate::workbench::editor::EditorView; -use crate::workbench::sidebar::{SidebarEvent, SidebarTab, SidebarView}; +use crate::workbench::search_everywhere::{ + SearchEverywhereEvent, SearchEverywhereModal, +}; +use crate::workbench::settings_dialog::{SettingsDialog, SettingsEvent}; +use crate::workbench::sidebar::{FileEntry, SidebarEvent, SidebarTab, SidebarView}; use crate::workbench::status_bar::StatusBarView; use crate::workbench::toolbar::{ToolbarEvent, ToolbarView}; -/// Linux 前端主工作台视图组件(复刻 macOS LitheTheme / Tauri 架构) +/// Linux 前端主工作台视图组件(复刻 macOS LitheTheme / IntelliJ 架构) pub struct WorkbenchView { /// 项目工作区根路径 pub workspace_root: String, /// 侧边栏是否展开 pub sidebar_visible: bool, + /// 侧边栏宽度(默认 260.0,支持拖拽调节) + pub sidebar_width: f32, + /// 底部面板高度(默认 240.0,支持拖拽调节) + pub bottom_panel_height: f32, + /// 是否正在调节侧边栏宽度 + pub is_resizing_sidebar: bool, + /// 是否正在调节底部面板高度 + pub is_resizing_bottom_panel: bool, + drag_start_x: f32, + drag_start_y: f32, + initial_resize_size: f32, + + /// 全局搜索弹窗浮层是否可见 + pub show_search_everywhere: bool, + /// 设置模态对话框是否可见 + pub show_settings_dialog: bool, + /// 顶部标题栏/工具栏 pub toolbar: Entity, /// 左侧垂直活动栏 (Activity Rail) @@ -32,6 +54,12 @@ pub struct WorkbenchView { pub bottom_panel: Entity, /// 底部状态栏 (Status Bar) pub status_bar: Entity, + /// 全局搜索弹窗 + pub search_everywhere: Entity, + /// 设置模态对话框 + pub settings_dialog: Entity, + + focus_handle: FocusHandle, client: CoreClient, _subscriptions: Vec, } @@ -47,7 +75,7 @@ impl WorkbenchView { pub fn with_root( workspace_root: String, - _window: &mut Window, + window: &mut Window, cx: &mut Context, ) -> Self { let root = workspace_root.clone(); @@ -55,13 +83,17 @@ impl WorkbenchView { let toolbar = cx.new(|_cx| ToolbarView::new(&root)); let activity_rail = cx.new(|_cx| ActivityRailView::new()); let sidebar = cx.new(|cx| SidebarView::new(root.clone(), cx)); - let editor = cx.new(|cx| EditorView::new(root.clone(), cx)); + let editor = cx.new(|cx| EditorView::new(root.clone(), window, cx)); let bottom_panel = cx.new(|cx| BottomPanelView::new(root.clone(), cx)); let status_bar = cx.new(|_cx| StatusBarView::new()); + let search_everywhere = cx.new(|cx| SearchEverywhereModal::new(cx)); + let settings_dialog = cx.new(|_cx| SettingsDialog::new()); + let focus_handle = cx.focus_handle(); - // 1. 订阅侧边栏文件打开事件 + // 1. 订阅侧边栏事件(打开文件、新建文件、提交 Git) let status_bar_clone = status_bar.clone(); - let sub_sidebar = cx.subscribe(&sidebar, move |this, _sidebar, event: &SidebarEvent, cx| { + let bottom_panel_sidebar = bottom_panel.clone(); + let sub_sidebar = cx.subscribe(&sidebar, move |this, sidebar, event: &SidebarEvent, cx| { match event { SidebarEvent::OpenFile(path) => { this.open_file(path, cx); @@ -75,12 +107,39 @@ impl WorkbenchView { ); }); } + SidebarEvent::NewFile => { + let _ = this.editor.update(cx, |ed, cx| { + ed.open_file("untitled.txt".to_string(), String::new(), cx); + }); + let _ = status_bar_clone.update(cx, |sb, cx| { + sb.set_file_info( + Some("untitled.txt".to_string()), + 1, + 1, + "Plain Text".to_string(), + cx, + ); + }); + } + SidebarEvent::Commit(msg) => { + let log_msg = format!("[Git Commit] {}", msg); + let _ = bottom_panel_sidebar.update(cx, |bp, cx| { + bp.append_log(log_msg.clone(), cx); + let _ = bp.terminal.update(cx, |term, cx| { + term.send_command(&format!("git commit -m \"{}\"", msg), cx); + }); + }); + let _ = sidebar.update(cx, |sb, cx| { + sb.refresh_git(cx); + }); + } } }); - // 2. 观察侧边栏 Git 分支状态变化并同步至 Toolbar 与 StatusBar + // 2. 观察侧边栏状态变化(分支同步与工作区文件快照同步给全局搜索) let toolbar_branch_sync = toolbar.clone(); let status_bar_branch_sync = status_bar.clone(); + let search_sync = search_everywhere.clone(); let obs_sidebar = cx.observe(&sidebar, move |_this, sidebar, cx| { let branch = sidebar.read(cx).git_branch.clone(); let _ = toolbar_branch_sync.update(cx, |tb, cx| { @@ -89,6 +148,17 @@ impl WorkbenchView { let _ = status_bar_branch_sync.update(cx, |sb, cx| { sb.set_git_branch(branch, cx); }); + + // 收集所有文件供全局搜索 + let mut file_list = Vec::new(); + if let Some(root_node) = &sidebar.read(cx).root_node { + Self::collect_all_file_paths(root_node, &mut file_list); + } + if !file_list.is_empty() { + let _ = search_sync.update(cx, |search, cx| { + search.set_files(file_list, cx); + }); + } }); // 3. 订阅左侧活动栏 (Activity Rail) 事件 @@ -122,9 +192,8 @@ impl WorkbenchView { }); } ActivityRailEvent::OpenSettings => { - let _ = bottom_panel_clone.update(cx, |bp, cx| { - bp.append_log("[Settings] Settings dialog requested".to_string(), cx); - }); + this.show_settings_dialog = true; + cx.notify(); } } }); @@ -178,20 +247,13 @@ impl WorkbenchView { }); } ToolbarEvent::QuickOpen => { - let _ = this.sidebar.update(cx, |sb, cx| { - sb.set_tab(SidebarTab::Search, cx); - }); - this.sidebar_visible = true; - let _ = this.activity_rail.update(cx, |r, cx| { - r.set_active_tab(Some(ActivityTab::Search), cx) - }); - cx.notify(); + this.open_search_everywhere(cx); } ToolbarEvent::Run => { let _ = this.bottom_panel.update(cx, |bp, cx| { bp.append_log("[Run] Executing default run configuration...".to_string(), cx); let _ = bp.terminal.update(cx, |term, cx| { - term.send_command("echo '[Lithe Run]' && ls -la", cx); + term.send_command("echo '[Lithe Run]' && cargo check", cx); }); }); } @@ -206,9 +268,8 @@ impl WorkbenchView { }); } ToolbarEvent::OpenSettings => { - let _ = this.bottom_panel.update(cx, |bp, cx| { - bp.append_log("[Settings] Settings dialog requested".to_string(), cx); - }); + this.show_settings_dialog = true; + cx.notify(); } ToolbarEvent::About => { let _ = this.bottom_panel.update(cx, |bp, cx| { @@ -221,17 +282,166 @@ impl WorkbenchView { } }); + // 5. 订阅全局搜索事件 (SearchEverywhereEvent) + let status_bar_search = status_bar.clone(); + let sub_search = cx.subscribe(&search_everywhere, move |this, _search, event: &SearchEverywhereEvent, cx| { + match event { + SearchEverywhereEvent::OpenFile(path) => { + this.open_file(path, cx); + let _ = status_bar_search.update(cx, |sb, cx| { + sb.set_file_info( + Some(path.clone()), + 1, + 1, + EditorView::language_name(path).to_string(), + cx, + ); + }); + this.show_search_everywhere = false; + cx.notify(); + } + SearchEverywhereEvent::ExecuteAction(action_id) => { + this.handle_action(action_id, cx); + this.show_search_everywhere = false; + cx.notify(); + } + SearchEverywhereEvent::Close => { + this.show_search_everywhere = false; + cx.notify(); + } + } + }); + + // 6. 订阅设置对话框事件 (SettingsEvent) + let sub_settings = cx.subscribe(&settings_dialog, |this, _dialog, event: &SettingsEvent, cx| { + match event { + SettingsEvent::Close => { + this.show_settings_dialog = false; + cx.notify(); + } + } + }); + Self { workspace_root, sidebar_visible: true, + sidebar_width: 260.0, + bottom_panel_height: 240.0, + is_resizing_sidebar: false, + is_resizing_bottom_panel: false, + drag_start_x: 0.0, + drag_start_y: 0.0, + initial_resize_size: 0.0, + show_search_everywhere: false, + show_settings_dialog: false, toolbar, activity_rail, sidebar, editor, bottom_panel, status_bar, + search_everywhere, + settings_dialog, + focus_handle, client: CoreClient::new(), - _subscriptions: vec![sub_sidebar, obs_sidebar, sub_rail, sub_toolbar], + _subscriptions: vec![ + sub_sidebar, + obs_sidebar, + sub_rail, + sub_toolbar, + sub_search, + sub_settings, + ], + } + } + + /// 打开全局搜索弹窗并更新当前文件索引 + pub fn open_search_everywhere(&mut self, cx: &mut Context) { + this_sync_files(self, cx); + self.show_search_everywhere = true; + let _ = self.search_everywhere.update(cx, |search, cx| { + search.reset(cx); + }); + cx.notify(); + } + + fn collect_all_file_paths(entry: &FileEntry, output: &mut Vec) { + if !entry.is_directory { + output.push(entry.path.clone()); + } + if let Some(children) = &entry.children { + for child in children { + Self::collect_all_file_paths(child, output); + } + } + } + + /// 执行常用命令动作 + pub fn handle_action(&mut self, action_id: &str, cx: &mut Context) { + match action_id { + "workbench.new_file" => { + let _ = self.editor.update(cx, |ed, cx| { + ed.open_file("untitled.txt".to_string(), String::new(), cx); + }); + } + "workbench.save" => { + let _ = self.editor.update(cx, |ed, cx| { + ed.save_active(cx); + }); + } + "workbench.close_tab" => { + let _ = self.editor.update(cx, |ed, cx| { + if let Some(idx) = ed.active_tab_index { + ed.close_tab(idx, cx); + } + }); + } + "workbench.toggle_terminal" => { + let _ = self.bottom_panel.update(cx, |bp, cx| { + bp.toggle_collapsed(cx); + }); + } + "workbench.toggle_sidebar" => { + self.sidebar_visible = !self.sidebar_visible; + let tab = if self.sidebar_visible { + Some(ActivityTab::Explorer) + } else { + None + }; + let _ = self.activity_rail.update(cx, |r, cx| r.set_active_tab(tab, cx)); + cx.notify(); + } + "workbench.open_settings" => { + self.show_settings_dialog = true; + cx.notify(); + } + "workbench.refresh_workspace" => { + let _ = self.sidebar.update(cx, |sb, cx| { + sb.refresh(cx); + sb.refresh_git(cx); + }); + } + "workbench.run" => { + let _ = self.bottom_panel.update(cx, |bp, cx| { + bp.append_log("[Run] Executing default run configuration...".to_string(), cx); + let _ = bp.terminal.update(cx, |term, cx| { + term.send_command("cargo check", cx); + }); + }); + } + "workbench.debug" => { + let _ = self.bottom_panel.update(cx, |bp, cx| { + bp.append_log("[Debug] Launching DAP debug session...".to_string(), cx); + }); + } + "workbench.clear_terminal" => { + let _ = self.bottom_panel.update(cx, |bp, cx| { + let _ = bp.terminal.update(cx, |term, cx| { + term.clear(cx); + }); + }); + } + _ => {} } } @@ -263,50 +473,193 @@ impl WorkbenchView { } } +fn this_sync_files(this: &WorkbenchView, cx: &mut Context) { + let mut file_list = Vec::new(); + if let Some(root_node) = &this.sidebar.read(cx).root_node { + WorkbenchView::collect_all_file_paths(root_node, &mut file_list); + } + if !file_list.is_empty() { + let _ = this.search_everywhere.update(cx, |search, cx| { + search.set_files(file_list, cx); + }); + } +} + impl Render for WorkbenchView { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - v_flex() + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .track_focus(&self.focus_handle) + .relative() .size_full() .bg(ThemeColors::bg_editor()) - .child( - // 1. 顶部标题栏/工具栏 (Toolbar) - self.toolbar.clone(), + .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { + let key = event.keystroke.key.as_str(); + // 键盘快捷键监听 + if event.keystroke.modifiers.control || event.keystroke.modifiers.platform { + match key { + "p" => { + this.open_search_everywhere(cx); + } + "," => { + this.show_settings_dialog = true; + cx.notify(); + } + "b" => { + this.sidebar_visible = !this.sidebar_visible; + let tab = if this.sidebar_visible { + Some(ActivityTab::Explorer) + } else { + None + }; + let _ = this.activity_rail.update(cx, |r, cx| r.set_active_tab(tab, cx)); + cx.notify(); + } + "`" => { + let _ = this.bottom_panel.update(cx, |bp, cx| { + bp.toggle_collapsed(cx); + }); + } + _ => {} + } + } else if key == "escape" { + if this.show_search_everywhere { + this.show_search_everywhere = false; + cx.notify(); + } else if this.show_settings_dialog { + this.show_settings_dialog = false; + cx.notify(); + } + } + })) + .on_mouse_move(cx.listener(|this, event: &MouseMoveEvent, _window, cx| { + if this.is_resizing_sidebar { + let current_x = f32::from(event.position.x); + let delta = current_x - this.drag_start_x; + let new_width = (this.initial_resize_size + delta).clamp(160.0, 600.0); + if (this.sidebar_width - new_width).abs() > 0.5 { + this.sidebar_width = new_width; + cx.notify(); + } + } else if this.is_resizing_bottom_panel { + let current_y = f32::from(event.position.y); + let delta = this.drag_start_y - current_y; + let new_height = (this.initial_resize_size + delta).clamp(120.0, 500.0); + if (this.bottom_panel_height - new_height).abs() > 0.5 { + this.bottom_panel_height = new_height; + let _ = this.bottom_panel.update(cx, |bp, cx| { + bp.set_height(new_height, cx); + }); + cx.notify(); + } + } + })) + .on_mouse_up( + MouseButton::Left, + cx.listener(|this, _event: &MouseUpEvent, _window, cx| { + if this.is_resizing_sidebar || this.is_resizing_bottom_panel { + this.is_resizing_sidebar = false; + this.is_resizing_bottom_panel = false; + cx.notify(); + } + }), ) .child( - // 2. 主工作区区域(ActivityRail + 侧边栏 + 编辑器主岛) - h_flex() - .flex_1() - .w_full() + v_flex() + .size_full() + .bg(ThemeColors::bg_editor()) .child( - // 左侧垂直图标活动栏 (Activity Rail) - self.activity_rail.clone(), + // 1. 顶部标题栏/工具栏 (Toolbar) + self.toolbar.clone(), ) - .when(self.sidebar_visible, |layout| { - layout.child( - // 侧边栏内容面板 (Explorer / Search / Git) - div() - .w(px(260.0)) - .h_full() - .flex_shrink_0() - .child(self.sidebar.clone()), - ) - }) .child( - // 中央编辑器主岛 - div() + // 2. 主工作区区域(ActivityRail + 侧边栏 + 可调节垂直Splitter + 编辑器主岛) + h_flex() .flex_1() - .h_full() - .bg(ThemeColors::bg_editor()) - .child(self.editor.clone()), + .w_full() + .child( + // 左侧垂直图标活动栏 (Activity Rail) + self.activity_rail.clone(), + ) + .when(self.sidebar_visible, |layout| { + layout + .child( + // 侧边栏内容面板 (Explorer / Search / Git) + div() + .w(px(self.sidebar_width)) + .h_full() + .flex_shrink_0() + .child(self.sidebar.clone()), + ) + .child( + // 侧边栏与主编辑器之间的精致 Splitter 分隔条 + div() + .id("sidebar-splitter") + .w(px(4.0)) + .h_full() + .cursor_col_resize() + .bg(if self.is_resizing_sidebar { + ThemeColors::accent_blue() + } else { + ThemeColors::border() + }) + .hover(|h| h.bg(ThemeColors::accent_blue())) + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, event: &MouseDownEvent, _window, cx| { + this.is_resizing_sidebar = true; + this.drag_start_x = f32::from(event.position.x); + this.initial_resize_size = this.sidebar_width; + cx.notify(); + }), + ), + ) + }) + .child( + // 中央编辑器主岛 + div() + .flex_1() + .h_full() + .bg(ThemeColors::bg_editor()) + .child(self.editor.clone()), + ), + ) + .child( + // 主编辑器与底栏之间的精致 Splitter 分隔条 + div() + .id("bottom-panel-splitter") + .h(px(4.0)) + .w_full() + .cursor_row_resize() + .bg(if self.is_resizing_bottom_panel { + ThemeColors::accent_blue() + } else { + ThemeColors::border() + }) + .hover(|h| h.bg(ThemeColors::accent_blue())) + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, event: &MouseDownEvent, _window, cx| { + this.is_resizing_bottom_panel = true; + this.drag_start_y = f32::from(event.position.y); + this.initial_resize_size = this.bottom_panel_height; + cx.notify(); + }), + ), + ) + .child( + // 3. 底部抽屉面板 (Terminal / Output / Problems) + self.bottom_panel.clone(), + ) + .child( + // 4. 最底部状态栏 (Status Bar) + self.status_bar.clone(), ), ) - .child( - // 3. 底部抽屉面板 (Terminal / Output / Problems) - self.bottom_panel.clone(), - ) - .child( - // 4. 最底部状态栏 (Status Bar) - self.status_bar.clone(), - ) + .when(self.show_search_everywhere, |view| { + view.child(self.search_everywhere.clone()) + }) + .when(self.show_settings_dialog, |view| { + view.child(self.settings_dialog.clone()) + }) } } From 93d8a1162d7d0a9a32ecaaec5f6bb4c9d5b0cfac Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 11:55:33 +0800 Subject: [PATCH 05/59] =?UTF-8?q?feat(linux):=20=E5=A4=8D=E5=88=BB=20Tauri?= =?UTF-8?q?=20=E4=B8=BB=E5=B7=A5=E4=BD=9C=E5=8F=B0=20UI=20=E6=A1=86?= =?UTF-8?q?=E6=9E=B6=E5=B9=B6=E6=8E=A5=E5=85=A5=20XDG=20=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E6=8C=81=E4=B9=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对齐 windows/tauri 的 UI 结构与交互,功能暂不接后端: - theme: 新增运行时调色板,移植 lithe-dark / lithe-light 全套 token, 保留旧 ThemeColors 访问器为别名,视图无需改动即可跟随主题。 - settings: 新增 XDG 设置持久化(~/.config/lithe/settings.json), 键名与 Tauri defaultSettings 对齐,含 coreFeatures / 顺序 / 布局尺寸; 提供 resolved_theme_id + apply_theme 支持明暗与跟随系统。 - toolbar: 顶栏复刻为紧凑菜单栏(File/Edit/View/Go/Terminal/Run/Tools/ Window/Help)+ 项目/分支胶囊 + 搜索条 + 运行控件 + 主题切换 + 窗口控件。 - activity_rail: 左侧活动栏 9 项(按 core_features 与设置顺序过滤/排序), 新增右侧插件活动栏(Extensions / Notifications 带角标 / Maven)。 - status_bar: 按 footerLeading/TrailingItemsOrder 渲染文件路径、分支、 光标、编码、缩进、只读、内存、Git 变更,变更 chip 可点击打开 Git。 - settings_dialog: 扩展为 12 分类(820x620),设置项读写 XDG 存储, 主题项即时应用并落盘,支持 Restore Defaults。 - command_palette / quick_open / welcome_screen: 新增三个浮层与欢迎页, 含分组列表、模式徽标、键盘导航与事件出口。 - main: 改用 AllAssets 以提供完整 Lucide 图标目录。 --- linux/src/lib.rs | 1 + linux/src/main.rs | 27 +- linux/src/settings.rs | 270 ++++ linux/src/theme.rs | 335 ++++- linux/src/workbench/activity_rail.rs | 393 ++++- linux/src/workbench/command_palette.rs | 912 +++++++++++ linux/src/workbench/mod.rs | 17 +- linux/src/workbench/quick_open.rs | 506 +++++++ linux/src/workbench/settings_dialog.rs | 1912 +++++++++++++++++------- linux/src/workbench/status_bar.rs | 299 ++-- linux/src/workbench/toolbar.rs | 533 +++++-- linux/src/workbench/view.rs | 1013 +++++++++---- linux/src/workbench/welcome_screen.rs | 544 +++++++ 13 files changed, 5591 insertions(+), 1171 deletions(-) create mode 100644 linux/src/settings.rs create mode 100644 linux/src/workbench/command_palette.rs create mode 100644 linux/src/workbench/quick_open.rs create mode 100644 linux/src/workbench/welcome_screen.rs diff --git a/linux/src/lib.rs b/linux/src/lib.rs index f8a5e12e9..a8ee881d4 100644 --- a/linux/src/lib.rs +++ b/linux/src/lib.rs @@ -1,4 +1,5 @@ pub mod core; +pub mod settings; pub mod theme; pub mod workbench; diff --git a/linux/src/main.rs b/linux/src/main.rs index e614fba38..c5d5a3990 100644 --- a/linux/src/main.rs +++ b/linux/src/main.rs @@ -1,4 +1,7 @@ +//! Linux 工作台入口:初始化设置与主题,打开主窗口。 + mod core; +mod settings; mod theme; mod workbench; @@ -7,11 +10,31 @@ use gpui_kit::*; use workbench::WorkbenchView; fn main() { - let app = gpui_kit::application().with_assets(gpui_kit::assets::Assets); + // 工作台使用大量 Lucide 图标,默认 `Assets` 仅内嵌 101 个,需用全量目录。 + let app = gpui_kit::application().with_assets(gpui_kit::assets::AllAssets); app.run(move |cx| { gpui_kit::init(cx); - Theme::change(ThemeMode::Dark, None, 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)); diff --git a/linux/src/settings.rs b/linux/src/settings.rs new file mode 100644 index 000000000..e3dfdd291 --- /dev/null +++ b/linux/src/settings.rs @@ -0,0 +1,270 @@ +//! 应用设置的 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 `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 `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_size: f32, + pub tab_size: usize, + pub word_wrap: bool, + pub line_numbers: bool, + pub show_minimap: bool, + // Terminal + pub terminal_scrollback: usize, + // UI + 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, + // 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_size: 13.0, + tab_size: 2, + word_wrap: false, + line_numbers: true, + show_minimap: true, + terminal_scrollback: 10000, + display_language: "zh-CN".to_string(), + 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, + 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(); + }; + serde_json::from_str::(&text).unwrap_or_default() +} + +/// 将设置写回磁盘(临时文件 + 原子替换)。 +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 snapshot = get(cx).clone(); + persist(&snapshot); + cx.refresh_windows(); +} + +/// 解析当前应生效的主题 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); +} diff --git a/linux/src/theme.rs b/linux/src/theme.rs index 73856b750..67fe1974e 100644 --- a/linux/src/theme.rs +++ b/linux/src/theme.rs @@ -1,104 +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: 1.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 { - pub const BG_TITLEBAR: Rgba = const_rgb(0x1e1f22); - pub const BG_ACTIVITY_RAIL: Rgba = const_rgb(0x18191c); - pub const BG_SIDEBAR: Rgba = const_rgb(0x1e1f22); - pub const BG_EDITOR: Rgba = const_rgb(0x2b2d30); - pub const BG_TAB_BAR: Rgba = const_rgb(0x1e1f22); - pub const BG_TAB_ACTIVE: Rgba = const_rgb(0x2b2d30); - pub const BG_TAB_HOVER: Rgba = const_rgb(0x26282c); - pub const BG_BOTTOM_PANEL: Rgba = const_rgb(0x1e1f22); - pub const BG_STATUSBAR: Rgba = const_rgb(0x18191c); - pub const BORDER: Rgba = const_rgb(0x323438); - pub const ACCENT_BLUE: Rgba = const_rgb(0x3574f0); - pub const ACCENT_GREEN: Rgba = const_rgb(0x39a36b); - pub const ACCENT_YELLOW: Rgba = const_rgb(0xd29922); - pub const ACCENT_RED: Rgba = const_rgb(0xe02e2a); - pub const TEXT_PRIMARY: Rgba = const_rgb(0xdfdfe0); - pub const TEXT_MUTED: Rgba = const_rgb(0x818594); - pub const SUBTLE_SELECTION: Rgba = const_rgb(0x26282d); + // ---- 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 { - Self::BG_TITLEBAR + palette().background } #[inline] pub fn bg_activity_rail() -> Rgba { - Self::BG_ACTIVITY_RAIL + palette().surface } #[inline] pub fn bg_sidebar() -> Rgba { - Self::BG_SIDEBAR + palette().surface } #[inline] pub fn bg_editor() -> Rgba { - Self::BG_EDITOR + palette().background } #[inline] pub fn bg_tab_bar() -> Rgba { - Self::BG_TAB_BAR + palette().background } #[inline] pub fn bg_tab_active() -> Rgba { - Self::BG_TAB_ACTIVE + palette().background } #[inline] pub fn bg_tab_hover() -> Rgba { - Self::BG_TAB_HOVER + palette().tab_hover() } #[inline] pub fn bg_bottom_panel() -> Rgba { - Self::BG_BOTTOM_PANEL + palette().background } #[inline] pub fn bg_statusbar() -> Rgba { - Self::BG_STATUSBAR - } - #[inline] - pub fn border() -> Rgba { - Self::BORDER + palette().background } #[inline] pub fn accent_blue() -> Rgba { - Self::ACCENT_BLUE + palette().primary } #[inline] pub fn accent_green() -> Rgba { - Self::ACCENT_GREEN + palette().success } #[inline] pub fn accent_yellow() -> Rgba { - Self::ACCENT_YELLOW + palette().warning } #[inline] pub fn accent_red() -> Rgba { - Self::ACCENT_RED + palette().destructive } #[inline] pub fn text_primary() -> Rgba { - Self::TEXT_PRIMARY + palette().foreground } #[inline] pub fn text_muted() -> Rgba { - Self::TEXT_MUTED + palette().subtle_foreground } #[inline] pub fn subtle_selection() -> Rgba { - Self::SUBTLE_SELECTION + palette().accent } } diff --git a/linux/src/workbench/activity_rail.rs b/linux/src/workbench/activity_rail.rs index 03a007567..5174d9bfa 100644 --- a/linux/src/workbench/activity_rail.rs +++ b/linux/src/workbench/activity_rail.rs @@ -1,126 +1,235 @@ +//! 活动栏:左侧主活动栏 + 右侧插件活动栏,复刻 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::v_flex; +use gpui_kit::component::{h_flex, v_flex}; use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::{ - px, Context, EventEmitter, IntoElement, ParentElement as _, Render, Styled as _, Window, + 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; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ActivityTab { - Explorer, - Git, - Search, -} - +/// 左侧活动栏事件。`SelectView` 只承载顶部视图类(files/git/search), +/// `ToggleBottomPane` 承载底部工具面板类,二者取值与 Tauri 的稳定 id 一致。 #[derive(Debug, Clone)] +#[allow(dead_code)] pub enum ActivityRailEvent { - SelectTab(ActivityTab), - ToggleTerminal, + /// 切换顶部侧边视图,载荷为 "files" | "git" | "search"。 + SelectView(String), + /// 切换底部工具窗口,载荷为 "maven" | "run" | "terminal" | "diagnostics" | "gitLog"。 + ToggleBottomPane(String), + /// 打开设置。 OpenSettings, } pub struct ActivityRailView { - pub active_tab: Option, + /// 当前激活的顶部视图 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>, } impl EventEmitter for ActivityRailView {} impl ActivityRailView { - pub fn new() -> Self { + /// 从设置构建活动栏项列表。 + /// + /// 构造期只做一次快照:设置变更由 `view.rs` 重建/重订阅处理,这里不持有 + /// `App`,避免把全局状态带进渲染路径。 + pub fn new(cx: &mut Context) -> Self { + 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)) + .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(); + Self { - active_tab: Some(ActivityTab::Explorer), + active_view: Some("files".to_string()), + active_bottom: None, + top_items, + bottom_items, } } - pub fn set_active_tab(&mut self, tab: Option, cx: &mut Context) { - self.active_tab = tab; + 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, tooltip) = item_meta(id); + let is_active = + self.active_view.as_deref() == Some(id) || self.active_bottom.as_deref() == Some(id); + let event = Self::event_for(id); + + // 用相对容器叠加左侧 2px 高亮竖条:Button 自身不便稳定地画出单边竖条, + // 且竖条需要压在按钮背景之上。hover 反馈由 ghost 变体内置的 accent 背景提供。 + div() + .w(px(32.0)) + .h(px(32.0)) + .relative() + .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::primary() + } else { + ThemeColors::subtle_foreground() + }) + .when(is_active, |btn| btn.bg(ThemeColors::selected())) + .on_click(cx.listener(move |_this, _event, _window, cx| { + cx.emit(event.clone()); + })), + ) + .when(is_active, |d| { + d.child( + div() + .absolute() + .left_0() + .top_0() + .bottom_0() + .w(px(2.0)) + .bg(ThemeColors::primary()), + ) + }) + .into_any_element() + } } impl Render for ActivityRailView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let active = self.active_tab; + 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(40.0)) .h_full() - .bg(ThemeColors::bg_activity_rail()) + .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() - .child(self.render_rail_button( - "rail-files", - IconName::Folder, - "Explorer", - active == Some(ActivityTab::Explorer), - ActivityRailEvent::SelectTab(ActivityTab::Explorer), - cx, - )) - .child(self.render_rail_button( - "rail-git", - IconName::GitBranch, - "Version Control", - active == Some(ActivityTab::Git), - ActivityRailEvent::SelectTab(ActivityTab::Git), - cx, - )) - .child(self.render_rail_button( - "rail-search", - IconName::Search, - "Search Everywhere", - active == Some(ActivityTab::Search), - ActivityRailEvent::SelectTab(ActivityTab::Search), - cx, - )), + // 顶部视图组 + v_flex().w_full().items_center().gap_1().children(top), ) .child( - // 底部工具按钮列表 - v_flex() - .w_full() - .items_center() - .gap_1() - .child(self.render_rail_button( - "rail-terminal", - IconName::Terminal, - "Terminal", - false, - ActivityRailEvent::ToggleTerminal, - cx, - )) - .child(self.render_rail_button( - "rail-settings", - IconName::Settings, - "Settings", - false, - ActivityRailEvent::OpenSettings, - cx, - )), + // 底部工具组 + v_flex().w_full().items_center().gap_1().children(bottom), ) } } -impl ActivityRailView { - fn render_rail_button( +/// 右侧插件活动栏事件。 +#[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: 3, + maven_available: true, + } + } + + /// 单个插件按钮:尺寸与左侧栏按钮一致;图标沿用按钮 Medium 尺寸(16px)。 + fn render_button( &self, id: &'static str, icon: IconName, tooltip: &'static str, - is_active: bool, - event: ActivityRailEvent, + event: PluginRailEvent, cx: &mut Context, - ) -> impl IntoElement { + ) -> AnyElement { Button::new(id) .ghost() .icon(icon) @@ -128,18 +237,136 @@ impl ActivityRailView { .w(px(32.0)) .h(px(32.0)) .rounded(px(4.0)) - .text_color(if is_active { - ThemeColors::accent_blue() - } else { - ThemeColors::text_muted() - }) - .when(is_active, |btn| { - btn.bg(ThemeColors::bg_sidebar()) - .border_l_2() - .border_color(ThemeColors::accent_blue()) - }) + .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, + "Extensions", + PluginRailEvent::OpenExtensions, + cx, + ); + let maven = self.maven_available.then(|| { + self.render_button( + "plugin-maven", + IconName::Box, + "Maven", + 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, + "Notifications", + 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 → 图标与 tooltip(含快捷键,文案对齐 Tauri)。 +fn item_meta(id: &str) -> (IconName, &'static str) { + match id { + "files" => (IconName::Files, "Files (Ctrl+Shift+E)"), + "git" => (IconName::GitBranch, "Version Control (Ctrl+Shift+G)"), + "search" => (IconName::Search, "Search (Ctrl+Shift+F)"), + "maven" => (IconName::Box, "Maven"), + "run" => (IconName::Play, "Run (Shift+F10)"), + "terminal" => (IconName::Terminal, "Terminal (Ctrl+J)"), + "diagnostics" => (IconName::TriangleAlert, "Problems (Ctrl+Shift+J)"), + "gitLog" => (IconName::GitGraph, "Git Graph (Alt+9)"), + _ => (IconName::Settings, "Settings"), + } +} + +/// 把持久化顺序规范化成合法且无重复的静态 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/command_palette.rs b/linux/src/workbench/command_palette.rs new file mode 100644 index 000000000..e0aa6c052 --- /dev/null +++ b/linux/src/workbench/command_palette.rs @@ -0,0 +1,912 @@ +//! 工作台命令面板(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(); + let total = self.filtered.len(); + + // 全屏半透明遮罩:点击空白处关闭 + 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() { + "Type a command...".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("No commands found"), + ) + }) + .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(category), + ) + .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() + } + } + })), + ) + .child( + // 3. 底部指引栏:左侧命令数、右侧操作提示 + h_flex() + .h(px(34.0)) + .w_full() + .items_center() + .justify_between() + .px_4() + .bg(ThemeColors::background()) + .border_t_1() + .border_color(ThemeColors::border()) + .child( + div() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child(format!("{} commands", total)), + ) + .child( + div() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child("↑↓ Navigate · ↵ Run · Esc Close"), + ), + ), + ) + } +} + +/// 快捷键/键位徽标。 +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/mod.rs b/linux/src/workbench/mod.rs index e397bbc73..811610eac 100644 --- a/linux/src/workbench/mod.rs +++ b/linux/src/workbench/mod.rs @@ -1,6 +1,8 @@ pub mod activity_rail; pub mod bottom_panel; +pub mod command_palette; pub mod editor; +pub mod quick_open; pub mod search_everywhere; pub mod settings_dialog; pub mod sidebar; @@ -8,23 +10,30 @@ pub mod status_bar; pub mod terminal; pub mod toolbar; pub mod view; +pub mod welcome_screen; #[allow(unused_imports)] -pub use activity_rail::{ActivityRailEvent, ActivityRailView, ActivityTab}; +pub use activity_rail::{ + ActivityRailEvent, ActivityRailView, PluginActivityRailView, PluginRailEvent, +}; #[allow(unused_imports)] pub use bottom_panel::BottomPanelView; #[allow(unused_imports)] +pub use command_palette::{CommandPaletteEvent, CommandPaletteModal}; +#[allow(unused_imports)] pub use editor::EditorView; #[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::StatusBarView; -#[allow(unused_imports)] -pub use terminal::TerminalView; +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::{RecentProject, WelcomeEvent, WelcomeScreenView}; diff --git a/linux/src/workbench/quick_open.rs b/linux/src/workbench/quick_open.rs new file mode 100644 index 000000000..9155d9dca --- /dev/null +++ b/linux/src/workbench/quick_open.rs @@ -0,0 +1,506 @@ +//! 工作台快速打开(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 { + /// 顶部输入行展示的模式徽标文案。 + fn badge_label(self) -> &'static str { + match self { + QuickOpenMode::File => "File", + QuickOpenMode::Symbol => "Symbols", + QuickOpenMode::WorkspaceSymbol => "Workspace Symbols", + } + } + + /// 列表为空时展示的提示文案。 + fn empty_label(self) -> &'static str { + match self { + QuickOpenMode::File => "No matching files", + QuickOpenMode::Symbol => "No matching symbols", + QuickOpenMode::WorkspaceSymbol => "No matching workspace symbols", + } + } +} + +/// 快速打开对外事件。 +#[derive(Debug, Clone)] +pub enum QuickOpenEvent { + /// 打开选中的文件或跳转到选中的符号,携带路径。 + OpenFile(String), + /// 请求关闭快速打开。 + Close, +} + +/// 列表渲染行:分组标题或文件项(携带该项在 `filtered` 中的位置)。 +enum QuickOpenRow { + Header(&'static str), + File(usize, String), +} + +/// 居中的快速打开模态,宽 704、高 420。 +pub struct QuickOpenModal { + pub query: String, + pub mode: QuickOpenMode, + pub files: Vec, + /// 命中筛选的文件路径,保持 `files` 的原始顺序。 + pub filtered: Vec, + pub selected_index: usize, + pub focus_handle: FocusHandle, + /// 最近打开的文件,用于优先分组展示。 + 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(), + recent: Vec::new(), + } + } + + /// 替换候选文件列表并重算筛选结果。 + pub fn set_files(&mut self, files: Vec, cx: &mut Context) { + self.files = files; + 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() + } + } + + /// 按有效查询串重算命中列表。 + fn recompute_filtered(&mut self) { + let q = self.effective_query(); + self.filtered = self + .files + .iter() + .filter(|path| q.is_empty() || path.to_lowercase().contains(&q)) + .cloned() + .collect(); + if self.selected_index >= self.filtered.len() { + self.selected_index = self.filtered.len().saturating_sub(1); + } + } + + /// 把命中列表展开为“recent / other”分组行序列。 + fn build_rows(&self) -> Vec { + let mut rows = Vec::new(); + let mut recent_emitted = false; + let mut other_emitted = false; + for (position, path) in self.filtered.iter().enumerate() { + if self.recent.iter().any(|r| r == path) { + if !recent_emitted { + rows.push(QuickOpenRow::Header("recent")); + recent_emitted = true; + } + } else if !other_emitted { + rows.push(QuickOpenRow::Header("other")); + other_emitted = true; + } + rows.push(QuickOpenRow::File(position, path.clone())); + } + rows + } + + /// 打开命中列表第 `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 total = self.filtered.len(); + let mode = self.mode; + + // 全屏半透明遮罩:点击空白处关闭 + 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(mode_badge(mode.badge_label())) + .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() { + "Search files by name (@ symbols, # workspace)" + .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. 结果列表:按 recent / other 分组,路径分段着色 + 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()), + ) + }) + .children(rows.into_iter().map(|row| { + match row { + QuickOpenRow::Header(label) => h_flex() + .px_3() + .pt_2() + .pb_1() + .child( + div() + .text_xs() + .font_weight(FontWeight::MEDIUM) + .text_color(ThemeColors::muted_foreground()) + .child(label), + ) + .into_any_element(), + 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( + Icon::new(IconName::FileText) + .size(px(14.0)) + .text_color(if is_selected { + ThemeColors::primary() + } else { + ThemeColors::muted_foreground() + }), + ) + .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() + } + } + })), + ) + .child( + // 3. 底部指引栏:左侧结果数、右侧操作提示 + h_flex() + .h(px(34.0)) + .w_full() + .items_center() + .justify_between() + .px_4() + .bg(ThemeColors::background()) + .border_t_1() + .border_color(ThemeColors::border()) + .child( + div() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child(format!("{} results", total)), + ) + .child( + div() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child("↑↓ Navigate · ↵ Open · Esc Close"), + ), + ), + ) + } +} + +/// 把路径拆成 `(目录, 文件名)`,无目录时目录为空串。 +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 mode_badge(label: &str) -> AnyElement { + div() + .px_2() + .py(px(2.0)) + .rounded_md() + .bg(ThemeColors::accent()) + .border_1() + .border_color(ThemeColors::border()) + .text_xs() + .font_weight(FontWeight::MEDIUM) + .text_color(ThemeColors::primary()) + .child(label.to_string()) + .into_any_element() +} + +/// 快捷键/键位徽标。 +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() +} diff --git a/linux/src/workbench/settings_dialog.rs b/linux/src/workbench/settings_dialog.rs index 14dde8f32..86b16465f 100644 --- a/linux/src/workbench/settings_dialog.rs +++ b/linux/src/workbench/settings_dialog.rs @@ -1,3 +1,12 @@ +//! 设置对话框:复刻 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::scroll::ScrollableElement as _; @@ -8,72 +17,227 @@ use gpui_kit::{ ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Window, }; +use crate::settings::{self, Settings}; use crate::theme::ThemeColors; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// 设置分类,顺序与 Tauri `categories` 数组一致。 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum SettingsCategory { + #[default] General, + Project, + Run, Editor, - Git, + 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", + } + } + + /// 中文标题,展示在导航项与内容区标题。 + pub fn title(self) -> &'static str { + match self { + SettingsCategory::General => "通用", + SettingsCategory::Project => "项目", + SettingsCategory::Run => "运行", + SettingsCategory::Editor => "编辑器", + SettingsCategory::Keyboard => "快捷键", + SettingsCategory::Terminal => "终端", + SettingsCategory::Lsp => "语言服务", + SettingsCategory::Ai => "AI 助手", + SettingsCategory::AiCommit => "AI 提交", + SettingsCategory::Git => "版本控制", + SettingsCategory::Logs => "日志", + SettingsCategory::Updates => "更新", + } + } + + /// 导航图标;只使用 `IconName` 中确定存在的变体。 + pub fn icon(self) -> IconName { + match self { + SettingsCategory::General => IconName::SlidersHorizontal, + SettingsCategory::Project => IconName::Folder, + SettingsCategory::Run => IconName::Play, + SettingsCategory::Editor => IconName::Code, + SettingsCategory::Keyboard => IconName::Keyboard, + SettingsCategory::Terminal => IconName::Terminal, + SettingsCategory::Lsp => IconName::Database, + SettingsCategory::Ai => IconName::Bot, + SettingsCategory::AiCommit => IconName::Bot, + SettingsCategory::Git => IconName::GitBranch, + 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, + } + } } +/// 对话框对外事件:关闭,以及设置已变更(宿主可据此同步其它视图)。 #[derive(Debug, Clone)] pub enum SettingsEvent { Close, + Changed, } -/// 居中设置面板(宽 640px,高 440px) +/// 居中设置面板(宽 820px,高 620px)。 pub struct SettingsDialog { + /// 当前激活分类 pub active_category: SettingsCategory, - // General 设置 - pub theme_name: String, - pub ui_scale: String, - pub language: String, - // Editor 设置 - pub font_size: usize, - pub tab_size: usize, - pub show_line_numbers: bool, - pub word_wrap: bool, - // Git 设置 - pub git_auto_fetch: bool, - pub git_format_on_commit: bool, - pub git_gpg_sign: bool, - // Terminal 设置 - pub default_shell: String, - pub scrollback_lines: usize, + /// 当前工作区根路径,Project 分类只读展示 + workspace_root: String, + // 以下字段对应 Tauri 设置但 Linux `Settings` 尚未提供,暂作对话框本地占位。 + open_folders_in_new_window: bool, + keybinding_preset: String, + vim_mode: bool, + vim_relative_line_numbers: bool, + terminal_cursor_style: String, + lsp_auto_completion: bool, + lsp_parameter_hints: bool, + auto_refresh_git_status: bool, + show_untracked_files: bool, + show_staged_first: bool, + git_default_diff_view: String, + enable_inline_git_blame: bool, + log_level: String, } impl EventEmitter for SettingsDialog {} impl SettingsDialog { - pub fn new() -> Self { + /// 从持久化设置读取上次打开的分类;同时缓存工作区路径供 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: SettingsCategory::General, - theme_name: "Dark Default".to_string(), - ui_scale: "100%".to_string(), - language: "English".to_string(), - font_size: 14, - tab_size: 4, - show_line_numbers: true, - word_wrap: false, - git_auto_fetch: true, - git_format_on_commit: true, - git_gpg_sign: false, - default_shell: std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string()), - scrollback_lines: 1000, + active_category, + workspace_root, + open_folders_in_new_window: true, + keybinding_preset: "none".to_string(), + vim_mode: false, + vim_relative_line_numbers: false, + terminal_cursor_style: "bar".to_string(), + lsp_auto_completion: true, + lsp_parameter_hints: true, + auto_refresh_git_status: true, + show_untracked_files: true, + show_staged_first: true, + git_default_diff_view: "unified".to_string(), + enable_inline_git_blame: true, + log_level: "info".to_string(), } } + /// 切换分类并把 `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()); + cx.notify(); + } + + /// 打开对话框时按持久化的 `lastSettingsTab` 重置分类。 + pub fn open(&mut self, cx: &mut Context) { + self.active_category = SettingsCategory::from_id(&settings::get(cx).last_settings_tab); + cx.notify(); + } + + /// 恢复占位项的本地默认值(不涉及持久化)。 + fn reset_placeholders(&mut self) { + self.open_folders_in_new_window = true; + self.keybinding_preset = "none".to_string(); + self.vim_mode = false; + self.vim_relative_line_numbers = false; + self.terminal_cursor_style = "bar".to_string(); + self.lsp_auto_completion = true; + self.lsp_parameter_hints = true; + self.auto_refresh_git_status = true; + self.show_untracked_files = true; + self.show_staged_first = true; + self.git_default_diff_view = "unified".to_string(); + self.enable_inline_git_blame = true; + self.log_level = "info".to_string(); + } + + /// 写入设置、广播 `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(); } } impl Render for SettingsDialog { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - // 全屏半透明遮罩 + // 全屏半透明遮罩:点击遮罩关闭,点击卡片不冒泡。 div() .id("settings-dialog-backdrop") .absolute() @@ -89,12 +253,11 @@ impl Render for SettingsDialog { }), ) .child( - // 居中模态面板:宽 640px,高 440px v_flex() .id("settings-dialog-card") - .w(px(640.0)) - .h(px(440.0)) - .bg(ThemeColors::bg_sidebar()) + .w(px(820.0)) + .h(px(620.0)) + .bg(ThemeColors::surface()) .border_1() .border_color(ThemeColors::border()) .rounded_lg() @@ -106,17 +269,18 @@ impl Render for SettingsDialog { // 阻止向遮罩冒泡 }), ) + // 1. 头部:标题、搜索占位、关闭按钮 .child( - // 1. 头部标题栏:带 IconName::Settings 标题与 IconName::Close 关闭按钮 h_flex() - .h(px(42.0)) + .h(px(44.0)) .w_full() .items_center() .justify_between() - .px_4() + .gap_3() + .px_3() .border_b_1() .border_color(ThemeColors::border()) - .bg(ThemeColors::bg_titlebar()) + .bg(ThemeColors::surface()) .child( h_flex() .items_center() @@ -124,18 +288,42 @@ impl Render for SettingsDialog { .child( Icon::new(IconName::Settings) .size(px(16.0)) - .text_color(ThemeColors::accent_blue()), + .text_color(ThemeColors::primary()), ) .child( div() .text_sm() .font_weight(FontWeight::BOLD) - .text_color(ThemeColors::text_primary()) + .text_color(ThemeColors::foreground()) .child("Settings"), ), ) .child( - Button::new("close-settings-btn") + // 搜索输入框占位:仅展示,不接输入。 + 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("搜索设置"), + ), + ) + .child( + Button::new("settings-close") .small() .ghost() .icon(IconName::Close) @@ -145,550 +333,1248 @@ impl Render for SettingsDialog { })), ), ) + // 2. 主体:左侧分类导航 + 右侧滚动内容 .child( - // 2. 主体区(左侧分类导航 + 右侧设置内容) h_flex() .flex_1() .w_full() + .min_h_0() + .overflow_hidden() .child( - // 左侧分类导航 v_flex() - .w(px(160.0)) + .w(px(190.0)) .h_full() - .bg(ThemeColors::bg_sidebar()) + .flex_shrink_0() + .gap_0p5() + .p_2() .border_r_1() .border_color(ThemeColors::border()) - .py_2() - .gap_1() - .child(self.render_nav_item( - "nav-general", - IconName::SlidersHorizontal, - "General", - self.active_category == SettingsCategory::General, - SettingsCategory::General, - cx, - )) - .child(self.render_nav_item( - "nav-editor", - IconName::FileCode, - "Editor", - self.active_category == SettingsCategory::Editor, - SettingsCategory::Editor, - cx, - )) - .child(self.render_nav_item( - "nav-git", - IconName::GitBranch, - "Git", - self.active_category == SettingsCategory::Git, - SettingsCategory::Git, - cx, - )) - .child(self.render_nav_item( - "nav-terminal", - IconName::Terminal, - "Terminal", - self.active_category == SettingsCategory::Terminal, - SettingsCategory::Terminal, - cx, - )), + .bg(ThemeColors::surface()) + .children( + SettingsCategory::ALL + .iter() + .map(|cat| self.render_nav_item(*cat, cx)), + ), ) .child( - // 右侧设置内容详情 div() .flex_1() .h_full() - .bg(ThemeColors::bg_editor()) + .min_w_0() + .bg(ThemeColors::background()) .p_4() .overflow_y_scrollbar() - .child(match self.active_category { - SettingsCategory::General => { - self.render_general_settings(cx).into_any_element() - } - SettingsCategory::Editor => { - self.render_editor_settings(cx).into_any_element() - } - SettingsCategory::Git => { - self.render_git_settings(cx).into_any_element() - } - SettingsCategory::Terminal => { - self.render_terminal_settings(cx).into_any_element() - } - }), + .child( + v_flex() + .w_full() + .gap_4() + .child( + div() + .text_lg() + .font_weight(FontWeight::SEMIBOLD) + .text_color(ThemeColors::foreground()) + .child(self.active_category.title()), + ) + .child(match self.active_category { + SettingsCategory::General => self + .render_general_content(cx) + .into_any_element(), + SettingsCategory::Project => self + .render_project_content(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(cx).into_any_element() + } + SettingsCategory::AiCommit => self + .render_ai_commit_content(cx) + .into_any_element(), + SettingsCategory::Git => { + self.render_git_content(cx).into_any_element() + } + SettingsCategory::Logs => { + self.render_logs_content(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("Restore Defaults") + .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("Done") + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(SettingsEvent::Close); + })), ), ), ) } } +/// 通用渲染块:分组、行、控件与分类内容。 impl SettingsDialog { - fn render_nav_item( - &self, - id: &'static str, - icon: IconName, - label: &'static str, - is_active: bool, - cat: SettingsCategory, - cx: &mut Context, - ) -> impl IntoElement { + /// 左侧导航项: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(id) + .id(cat.id()) .h(px(32.0)) .w_full() .items_center() .gap_2p5() - .px_3() + .px_2p5() + .rounded_sm() + .border_l_2() .cursor_pointer() .text_xs() .when(is_active, |row| { - row.bg(ThemeColors::subtle_selection()) - .border_l_2() - .border_color(ThemeColors::accent_blue()) - .text_color(ThemeColors::text_primary()) + row.bg(ThemeColors::accent()) + .border_color(ThemeColors::primary()) + .text_color(ThemeColors::foreground()) .font_weight(FontWeight::BOLD) }) .when(!is_active, |row| { - row.text_color(ThemeColors::text_muted()).hover(|h| { - h.bg(ThemeColors::bg_tab_hover()) - .text_color(ThemeColors::text_primary()) - }) + row.border_color(rgba(0x00000000)) + .text_color(ThemeColors::subtle_foreground()) + .hover(|h| { + h.bg(ThemeColors::accent()) + .text_color(ThemeColors::foreground()) + }) }) .child( - Icon::new(icon) + Icon::new(cat.icon()) .size(px(14.0)) .text_color(if is_active { - ThemeColors::accent_blue() + ThemeColors::foreground() } else { - ThemeColors::text_muted() + ThemeColors::subtle_foreground() }), ) - .child(label) + .child(cat.title()) .on_click(cx.listener(move |this, _event, _window, cx| { this.set_category(cat, cx); })) } - fn render_general_settings(&self, cx: &mut Context) -> impl IntoElement { + /// 分组容器:标题条 + 内容区,对应 Tauri `SettingsGroup`。 + fn render_group(&self, title: &'static str, content: impl IntoElement) -> impl IntoElement { v_flex() - .size_full() - .gap_4() - .child(self.render_section_title("Appearance & General")) - // 外观主题 - .child(self.render_setting_row( - "Theme", - "Color theme for editor and workbench interface", - h_flex() - .gap_1p5() - .child(self.render_choice_pill( - "theme-dark", - "Dark Default", - self.theme_name == "Dark Default", - cx.listener(|this, _event, _window, cx| { - this.theme_name = "Dark Default".to_string(); - cx.notify(); - }), - )) - .child(self.render_choice_pill( - "theme-contrast", - "High Contrast", - self.theme_name == "High Contrast", - cx.listener(|this, _event, _window, cx| { - this.theme_name = "High Contrast".to_string(); - cx.notify(); - }), - )), - )) - // 界面缩放 - .child(self.render_setting_row( - "UI Scale", - "Display scaling factor for the entire workspace", - h_flex() - .gap_1p5() - .child(self.render_choice_pill( - "scale-100", - "100%", - self.ui_scale == "100%", - cx.listener(|this, _event, _window, cx| { - this.ui_scale = "100%".to_string(); - cx.notify(); - }), - )) - .child(self.render_choice_pill( - "scale-125", - "125%", - self.ui_scale == "125%", - cx.listener(|this, _event, _window, cx| { - this.ui_scale = "125%".to_string(); - cx.notify(); - }), - )), - )) - // 语言 - .child(self.render_setting_row( - "Language", - "Application display and localization language", - h_flex() - .gap_1p5() - .child(self.render_choice_pill( - "lang-en", - "English", - self.language == "English", - cx.listener(|this, _event, _window, cx| { - this.language = "English".to_string(); - cx.notify(); - }), - )) - .child(self.render_choice_pill( - "lang-zh", - "简体中文", - self.language == "简体中文", - cx.listener(|this, _event, _window, cx| { - this.language = "简体中文".to_string(); - cx.notify(); - }), - )), - )) - } - - fn render_editor_settings(&self, cx: &mut Context) -> impl IntoElement { - v_flex() - .size_full() - .gap_4() - .child(self.render_section_title("Editor Settings")) - // 字体大小 - .child(self.render_setting_row( - "Font Size", - "Controls editor text font size in pixels", - h_flex() - .gap_1p5() - .child(self.render_choice_pill( - "font-13", - "13px", - self.font_size == 13, - cx.listener(|this, _event, _window, cx| { - this.font_size = 13; - cx.notify(); - }), - )) - .child(self.render_choice_pill( - "font-14", - "14px", - self.font_size == 14, - cx.listener(|this, _event, _window, cx| { - this.font_size = 14; - cx.notify(); - }), - )) - .child(self.render_choice_pill( - "font-16", - "16px", - self.font_size == 16, - cx.listener(|this, _event, _window, cx| { - this.font_size = 16; - cx.notify(); - }), - )), - )) - // Tab 缩进 - .child(self.render_setting_row( - "Tab Size", - "Number of spaces per indentation level", - h_flex() - .gap_1p5() - .child(self.render_choice_pill( - "tab-2", - "2 Spaces", - self.tab_size == 2, - cx.listener(|this, _event, _window, cx| { - this.tab_size = 2; - cx.notify(); - }), - )) - .child(self.render_choice_pill( - "tab-4", - "4 Spaces", - self.tab_size == 4, - cx.listener(|this, _event, _window, cx| { - this.tab_size = 4; - cx.notify(); - }), - )), - )) - // 显示行号 - .child(self.render_setting_row( - "Line Numbers", - "Display line number gutter in code editor", - h_flex() - .gap_1p5() - .child(self.render_choice_pill( - "ln-show", - "Show", - self.show_line_numbers, - cx.listener(|this, _event, _window, cx| { - this.show_line_numbers = true; - cx.notify(); - }), - )) - .child(self.render_choice_pill( - "ln-hide", - "Hide", - !self.show_line_numbers, - cx.listener(|this, _event, _window, cx| { - this.show_line_numbers = false; - cx.notify(); - }), - )), - )) - // 自动换行 - .child(self.render_setting_row( - "Word Wrap", - "Wrap long lines to fit the editor viewport width", - h_flex() - .gap_1p5() - .child(self.render_choice_pill( - "wrap-on", - "On", - self.word_wrap, - cx.listener(|this, _event, _window, cx| { - this.word_wrap = true; - cx.notify(); - }), - )) - .child(self.render_choice_pill( - "wrap-off", - "Off", - !self.word_wrap, - cx.listener(|this, _event, _window, cx| { - this.word_wrap = false; - cx.notify(); - }), - )), - )) - } - - fn render_git_settings(&self, cx: &mut Context) -> impl IntoElement { - v_flex() - .size_full() - .gap_4() - .child(self.render_section_title("Version Control & Git")) - // 自动拉取 - .child(self.render_setting_row( - "Auto Fetch", - "Automatically fetch remote branch status periodically", - h_flex() - .gap_1p5() - .child(self.render_choice_pill( - "fetch-on", - "Enabled", - self.git_auto_fetch, - cx.listener(|this, _event, _window, cx| { - this.git_auto_fetch = true; - cx.notify(); - }), - )) - .child(self.render_choice_pill( - "fetch-off", - "Disabled", - !self.git_auto_fetch, - cx.listener(|this, _event, _window, cx| { - this.git_auto_fetch = false; - cx.notify(); - }), - )), - )) - // Commit 前格式化 - .child(self.render_setting_row( - "Format on Commit", - "Run code formatter on staged changes before committing", - h_flex() - .gap_1p5() - .child(self.render_choice_pill( - "format-on", - "Enabled", - self.git_format_on_commit, - cx.listener(|this, _event, _window, cx| { - this.git_format_on_commit = true; - cx.notify(); - }), - )) - .child(self.render_choice_pill( - "format-off", - "Disabled", - !self.git_format_on_commit, - cx.listener(|this, _event, _window, cx| { - this.git_format_on_commit = false; - cx.notify(); - }), - )), - )) - // GPG 签名 - .child(self.render_setting_row( - "GPG Commit Signing", - "Sign commits with user default GPG signing key", - h_flex() - .gap_1p5() - .child(self.render_choice_pill( - "gpg-on", - "Enabled", - self.git_gpg_sign, - cx.listener(|this, _event, _window, cx| { - this.git_gpg_sign = true; - cx.notify(); - }), - )) - .child(self.render_choice_pill( - "gpg-off", - "Disabled", - !self.git_gpg_sign, - cx.listener(|this, _event, _window, cx| { - this.git_gpg_sign = false; - cx.notify(); - }), - )), - )) - } - - fn render_terminal_settings(&self, cx: &mut Context) -> impl IntoElement { - v_flex() - .size_full() - .gap_4() - .child(self.render_section_title("Terminal & PTY")) - // 默认终端 - .child(self.render_setting_row( - "Default Shell", - "System shell binary used for bottom terminal panel", - h_flex() - .gap_1p5() - .child(self.render_choice_pill( - "shell-bash", - "/bin/bash", - self.default_shell.contains("bash"), - cx.listener(|this, _event, _window, cx| { - this.default_shell = "/bin/bash".to_string(); - cx.notify(); - }), - )) - .child(self.render_choice_pill( - "shell-zsh", - "/bin/zsh", - self.default_shell.contains("zsh"), - cx.listener(|this, _event, _window, cx| { - this.default_shell = "/bin/zsh".to_string(); - cx.notify(); - }), - )) - .child(self.render_choice_pill( - "shell-sh", - "/bin/sh", - self.default_shell == "/bin/sh", - cx.listener(|this, _event, _window, cx| { - this.default_shell = "/bin/sh".to_string(); - cx.notify(); - }), - )), - )) - // 滚动缓冲区大小 - .child(self.render_setting_row( - "Scrollback Buffer", - "Maximum number of terminal output lines kept in memory", - h_flex() - .gap_1p5() - .child(self.render_choice_pill( - "scroll-1000", - "1000 lines", - self.scrollback_lines == 1000, - cx.listener(|this, _event, _window, cx| { - this.scrollback_lines = 1000; - cx.notify(); - }), - )) - .child(self.render_choice_pill( - "scroll-5000", - "5000 lines", - self.scrollback_lines == 5000, - cx.listener(|this, _event, _window, cx| { - this.scrollback_lines = 5000; - cx.notify(); - }), - )), - )) - } - - fn render_section_title(&self, title: &'static str) -> impl IntoElement { - div() - .text_sm() - .font_weight(FontWeight::BOLD) - .text_color(ThemeColors::text_primary()) - .pb_1() - .border_b_1() + .w_full() + .rounded_md() + .border_1() .border_color(ThemeColors::border()) - .child(title) + .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)) } - fn render_setting_row( + /// 设置行:左侧标签与描述,右侧控件。 + fn render_row( &self, label: &'static str, - desc: &'static str, + description: Option<&'static str>, 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() - .justify_between() .gap_4() - .child( - v_flex() - .flex_1() - .gap_0p5() - .child( - div() - .text_xs() - .font_weight(FontWeight::MEDIUM) - .text_color(ThemeColors::text_primary()) - .child(label), - ) - .child( - div() - .text_xs() - .text_color(ThemeColors::text_muted()) - .child(desc), - ), - ) - .child(control) + .child(info) + .child(div().flex_shrink_0().child(control)) } - fn render_choice_pill( + /// 开关控件:可点击药丸,On/Off 两种状态。 + 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() + })) + .child(if value { "On" } else { "Off" }) + .on_click(cx.listener(move |this, _event, _window, cx| toggle(this, cx))) + } + + /// 单选项:分段药丸,选中态用 primary 背景。 + fn render_segment( &self, id: &'static str, label: &'static str, - is_selected: bool, - click_handler: impl Fn(&gpui_kit::ClickEvent, &mut Window, &mut gpui_kit::App) + 'static, + selected: bool, + cx: &mut Context, + on_select: impl Fn(&mut Self, &mut Context) + 'static, ) -> impl IntoElement { h_flex() .id(id) .items_center() + .justify_center() .px_2p5() - .py(px(3.0)) - .rounded(px(4.0)) + .py_1() + .rounded_sm() .border_1() .cursor_pointer() .text_xs() - .when(is_selected, |pill| { - pill.bg(ThemeColors::accent_blue()) - .border_color(ThemeColors::accent_blue()) - .text_color(ThemeColors::text_primary()) - .font_weight(FontWeight::BOLD) + .when(selected, |el| { + el.bg(ThemeColors::primary()) + .border_color(ThemeColors::primary()) + .text_color(ThemeColors::foreground()) + .font_weight(FontWeight::MEDIUM) }) - .when(!is_selected, |pill| { - pill.bg(ThemeColors::bg_tab_bar()) + .when(!selected, |el| { + el.bg(ThemeColors::background()) .border_color(ThemeColors::border()) - .text_color(ThemeColors::text_muted()) + .text_color(ThemeColors::subtle_foreground()) .hover(|h| { - h.bg(ThemeColors::bg_tab_hover()) - .text_color(ThemeColors::text_primary()) + h.bg(ThemeColors::accent()) + .text_color(ThemeColors::foreground()) }) }) .child(label) - .on_click(click_handler) + .on_click(cx.listener(move |this, _event, _window, cx| on_select(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) + } + + /// 键位徽标:Keyboard 分类展示用,点击不触发录制。 + fn render_key_badge(&self, label: &'static str) -> impl IntoElement { + div() + .px_2() + .py_0p5() + .rounded_sm() + .border_1() + .border_color(ThemeColors::border()) + .bg(ThemeColors::background()) + .text_xs() + .text_color(ThemeColors::muted_foreground()) + .child(label) + } + + /// 占位空态:图标 + 说明。 + fn render_empty_state(&self, icon: IconName, text: &'static str) -> impl IntoElement { + v_flex() + .w_full() + .items_center() + .justify_center() + .gap_2() + .py_8() + .child( + Icon::new(icon) + .size(px(28.0)) + .text_color(ThemeColors::subtle_foreground()), + ) + .child( + div() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child(text), + ) + } + + /// 分组内的说明文本。 + fn render_note(&self, text: &'static str) -> impl IntoElement { + div() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child(text) + } +} + +/// 各分类内容渲染。 +impl SettingsDialog { + fn render_general_content(&self, cx: &mut Context) -> impl IntoElement { + let s = settings::get(cx).clone(); + v_flex() + .w_full() + .gap_4() + .child( + self.render_group( + "外观", + v_flex() + .w_full() + .gap_3() + .child( + self.render_row( + "主题", + Some("编辑器与工作台的配色方案"), + h_flex() + .gap_1p5() + .child(self.render_segment( + "theme-dark", + "深色", + s.theme == "lithe-dark", + cx, + |this, cx| { + this.commit_theme(cx, |s| { + s.theme = "lithe-dark".to_string() + }) + }, + )) + .child(self.render_segment( + "theme-light", + "浅色", + s.theme == "lithe-light", + cx, + |this, cx| { + this.commit_theme(cx, |s| { + s.theme = "lithe-light".to_string() + }) + }, + )), + ), + ) + .child(self.render_row( + "跟随系统主题", + Some("按系统外观在浅色与深色主题间自动切换"), + self.render_toggle( + "sync-system-theme", + s.sync_system_theme, + cx, + |this, cx| { + this.commit_theme(cx, |s| { + s.sync_system_theme = !s.sync_system_theme + }) + }, + ), + )) + .child( + self.render_row( + "窗口密度", + Some("标题栏与工具栏的紧凑程度"), + h_flex() + .gap_1p5() + .child(self.render_segment( + "chrome-focused", + "聚焦", + s.window_chrome_density == "focused", + cx, + |this, cx| { + this.commit(cx, |s| { + s.window_chrome_density = "focused".to_string() + }) + }, + )) + .child(self.render_segment( + "chrome-comfortable", + "舒适", + s.window_chrome_density == "comfortable", + cx, + |this, cx| { + this.commit(cx, |s| { + s.window_chrome_density = "comfortable".to_string() + }) + }, + )), + ), + ) + .child(self.render_row( + "减少动画", + Some("关闭过渡与动画以降低干扰"), + self.render_toggle("reduce-motion", s.reduce_motion, cx, |this, cx| { + this.commit(cx, |s| s.reduce_motion = !s.reduce_motion) + }), + )) + .child(self.render_row( + "紧凑菜单栏", + None, + self.render_toggle( + "compact-menu-bar", + s.compact_menu_bar, + cx, + |this, cx| { + this.commit(cx, |s| s.compact_menu_bar = !s.compact_menu_bar) + }, + ), + )) + .child(self.render_row( + "显示状态栏", + None, + self.render_toggle( + "show-status-bar", + s.show_status_bar, + cx, + |this, cx| { + this.commit(cx, |s| s.show_status_bar = !s.show_status_bar) + }, + ), + )), + ), + ) + .child( + self.render_group( + "文件与语言", + v_flex() + .w_full() + .gap_3() + .child(self.render_row( + "自动保存", + Some("编辑停止后自动保存当前文件"), + self.render_toggle("auto-save", s.auto_save, cx, |this, cx| { + this.commit(cx, |s| s.auto_save = !s.auto_save) + }), + )) + .child(self.render_row( + "快速打开预览", + Some("快速打开时以预览方式打开文件"), + self.render_toggle( + "quick-open-preview", + s.quick_open_preview, + cx, + |this, cx| { + this.commit(cx, |s| { + s.quick_open_preview = !s.quick_open_preview + }) + }, + ), + )) + .child( + self.render_row( + "显示语言", + None, + h_flex() + .gap_1p5() + .child(self.render_segment( + "lang-en", + "English", + s.display_language == "en-US", + cx, + |this, cx| { + this.commit(cx, |s| { + s.display_language = "en-US".to_string() + }) + }, + )) + .child(self.render_segment( + "lang-zh", + "简体中文", + s.display_language == "zh-CN", + cx, + |this, cx| { + this.commit(cx, |s| { + s.display_language = "zh-CN".to_string() + }) + }, + )), + ), + ), + ), + ) + } + + fn render_project_content(&self, cx: &mut Context) -> impl IntoElement { + v_flex() + .w_full() + .gap_4() + .child(self.render_group( + "当前工作区", + v_flex().w_full().gap_3().child(self.render_row( + "工作区路径", + Some("当前打开项目的根目录(只读)"), + self.render_value(self.workspace_root.clone()), + )), + )) + .child(self.render_group( + "项目打开方式", + v_flex().w_full().gap_3().child(self.render_row( + "在新窗口打开文件夹", + Some("占位项:Linux Settings 尚未提供该字段,暂不落盘"), + self.render_toggle( + "open-folders-in-new-window", + self.open_folders_in_new_window, + cx, + |this, cx| { + this.open_folders_in_new_window = !this.open_folders_in_new_window; + cx.notify(); + }, + ), + )), + )) + } + + fn render_run_content(&self, _cx: &mut Context) -> impl IntoElement { + v_flex().w_full().gap_4().child(self.render_group( + "运行配置", + v_flex().w_full().gap_3().child( + self.render_empty_state(IconName::Play, "暂无运行配置,创建后可在此管理启动项"), + ), + )) + } + + fn render_editor_content(&self, cx: &mut Context) -> impl IntoElement { + let s = settings::get(cx).clone(); + v_flex() + .w_full() + .gap_4() + .child( + self.render_group( + "显示", + v_flex() + .w_full() + .gap_3() + .child(self.render_row( + "字号", + Some("编辑器代码字号(11–20 px)"), + self.render_stepper( + "font-dec", + "font-inc", + format!("{} px", s.font_size as i32), + cx, + |this, cx| { + this.commit(cx, |s| s.font_size = (s.font_size - 1.0).max(11.0)) + }, + |this, cx| { + this.commit(cx, |s| s.font_size = (s.font_size + 1.0).min(20.0)) + }, + ), + )) + .child(self.render_row( + "显示行号", + None, + self.render_toggle( + "editor-line-numbers", + s.line_numbers, + cx, + |this, cx| this.commit(cx, |s| s.line_numbers = !s.line_numbers), + ), + )) + .child(self.render_row( + "自动换行", + None, + self.render_toggle("editor-word-wrap", s.word_wrap, cx, |this, cx| { + this.commit(cx, |s| s.word_wrap = !s.word_wrap) + }), + )) + .child(self.render_row( + "显示缩略图", + None, + self.render_toggle("editor-minimap", s.show_minimap, cx, |this, cx| { + this.commit(cx, |s| s.show_minimap = !s.show_minimap) + }), + )), + ), + ) + .child( + self.render_group( + "标签页", + v_flex() + .w_full() + .gap_3() + .child(self.render_row( + "显示标签图标", + None, + self.render_toggle( + "editor-tab-icons", + s.show_tab_icons, + cx, + |this, cx| { + this.commit(cx, |s| s.show_tab_icons = !s.show_tab_icons) + }, + ), + )) + .child( + self.render_row( + "关闭按钮显示", + Some("标签页关闭按钮的显示时机"), + h_flex() + .gap_1p5() + .child(self.render_segment( + "close-active", + "活动时", + s.tab_close_button_visibility == "active", + cx, + |this, cx| { + this.commit(cx, |s| { + s.tab_close_button_visibility = "active".to_string() + }) + }, + )) + .child(self.render_segment( + "close-hover", + "悬停时", + s.tab_close_button_visibility == "hover", + cx, + |this, cx| { + this.commit(cx, |s| { + s.tab_close_button_visibility = "hover".to_string() + }) + }, + )) + .child(self.render_segment( + "close-always", + "始终", + s.tab_close_button_visibility == "always", + cx, + |this, cx| { + this.commit(cx, |s| { + s.tab_close_button_visibility = "always".to_string() + }) + }, + )), + ), + ), + ), + ) + .child( + self.render_group( + "缩进", + v_flex().w_full().gap_3().child( + self.render_row( + "Tab 宽度", + None, + h_flex() + .gap_1p5() + .child(self.render_segment( + "tab-size-2", + "2 空格", + s.tab_size == 2, + cx, + |this, cx| this.commit(cx, |s| s.tab_size = 2), + )) + .child(self.render_segment( + "tab-size-4", + "4 空格", + s.tab_size == 4, + cx, + |this, cx| this.commit(cx, |s| s.tab_size = 4), + )) + .child(self.render_segment( + "tab-size-8", + "8 空格", + s.tab_size == 8, + cx, + |this, cx| this.commit(cx, |s| s.tab_size = 8), + )), + ), + ), + ), + ) + } + + fn render_keyboard_content(&self, cx: &mut Context) -> impl IntoElement { + v_flex() + .w_full() + .gap_4() + .child( + self.render_group( + "按键映射", + v_flex().w_full().gap_3().child( + self.render_row( + "预设", + Some("占位项:Linux Settings 尚未提供该字段,暂不落盘"), + h_flex() + .gap_1p5() + .child(self.render_segment( + "keymap-none", + "Lithe", + self.keybinding_preset == "none", + cx, + |this, cx| { + this.keybinding_preset = "none".to_string(); + cx.notify(); + }, + )) + .child(self.render_segment( + "keymap-vscode", + "VS Code", + self.keybinding_preset == "vscode", + cx, + |this, cx| { + this.keybinding_preset = "vscode".to_string(); + cx.notify(); + }, + )) + .child(self.render_segment( + "keymap-intellij", + "IntelliJ", + self.keybinding_preset == "intellij", + cx, + |this, cx| { + this.keybinding_preset = "intellij".to_string(); + cx.notify(); + }, + )), + ), + ), + ), + ) + .child( + self.render_group( + "Vim", + v_flex() + .w_full() + .gap_3() + .child(self.render_row( + "启用 Vim 模式", + None, + self.render_toggle("vim-mode", self.vim_mode, cx, |this, cx| { + this.vim_mode = !this.vim_mode; + cx.notify(); + }), + )) + .child(self.render_row( + "相对行号", + None, + self.render_toggle( + "vim-relative-line-numbers", + self.vim_relative_line_numbers, + cx, + |this, cx| { + this.vim_relative_line_numbers = + !this.vim_relative_line_numbers; + cx.notify(); + }, + ), + )), + ), + ) + .child( + self.render_group( + "快捷键", + v_flex() + .w_full() + .gap_3() + .child(self.render_row("保存文件", None, self.render_key_badge("Ctrl S"))) + .child(self.render_row("快速打开", None, self.render_key_badge("Ctrl P"))) + .child(self.render_row( + "全局搜索", + None, + self.render_key_badge("Ctrl Shift F"), + )) + .child(self.render_row("打开设置", None, self.render_key_badge("Ctrl ,"))) + .child(self.render_note("快捷键表为只读占位,暂不支持点击录制。")), + ), + ) + } + + fn render_terminal_content(&self, cx: &mut Context) -> impl IntoElement { + let s = settings::get(cx).clone(); + v_flex() + .w_full() + .gap_4() + .child( + self.render_group( + "回滚缓冲", + v_flex().w_full().gap_3().child( + self.render_row( + "缓冲行数", + Some("终端保留的最大输出行数"), + h_flex() + .gap_1p5() + .child(self.render_segment( + "scrollback-1000", + "1000", + s.terminal_scrollback == 1000, + cx, + |this, cx| this.commit(cx, |s| s.terminal_scrollback = 1000), + )) + .child(self.render_segment( + "scrollback-5000", + "5000", + s.terminal_scrollback == 5000, + cx, + |this, cx| this.commit(cx, |s| s.terminal_scrollback = 5000), + )) + .child(self.render_segment( + "scrollback-10000", + "10000", + s.terminal_scrollback == 10000, + cx, + |this, cx| this.commit(cx, |s| s.terminal_scrollback = 10000), + )), + ), + ), + ), + ) + .child( + self.render_group( + "光标", + v_flex().w_full().gap_3().child( + self.render_row( + "光标样式", + Some("占位项:Linux Settings 尚未提供该字段,暂不落盘"), + h_flex() + .gap_1p5() + .child(self.render_segment( + "cursor-bar", + "竖线", + self.terminal_cursor_style == "bar", + cx, + |this, cx| { + this.terminal_cursor_style = "bar".to_string(); + cx.notify(); + }, + )) + .child(self.render_segment( + "cursor-block", + "方块", + self.terminal_cursor_style == "block", + cx, + |this, cx| { + this.terminal_cursor_style = "block".to_string(); + cx.notify(); + }, + )) + .child(self.render_segment( + "cursor-underline", + "下划线", + self.terminal_cursor_style == "underline", + cx, + |this, cx| { + this.terminal_cursor_style = "underline".to_string(); + cx.notify(); + }, + )), + ), + ), + ), + ) + } + + fn render_lsp_content(&self, cx: &mut Context) -> impl IntoElement { + v_flex() + .w_full() + .gap_4() + .child( + self.render_group( + "语言服务", + v_flex() + .w_full() + .gap_3() + .child(self.render_row( + "自动补全", + Some("占位项:Linux Settings 尚未提供该字段,暂不落盘"), + self.render_toggle( + "lsp-auto-completion", + self.lsp_auto_completion, + cx, + |this, cx| { + this.lsp_auto_completion = !this.lsp_auto_completion; + cx.notify(); + }, + ), + )) + .child(self.render_row( + "参数提示", + None, + self.render_toggle( + "lsp-parameter-hints", + self.lsp_parameter_hints, + cx, + |this, cx| { + this.lsp_parameter_hints = !this.lsp_parameter_hints; + cx.notify(); + }, + ), + )), + ), + ) + .child( + self.render_group( + "已检测到的服务器", + v_flex() + .w_full() + .gap_3() + .child(self.render_row( + "Rust", + None, + self.render_value("rust-analyzer".to_string()), + )) + .child(self.render_row( + "TypeScript", + None, + self.render_value("typescript-language-server".to_string()), + )) + .child(self.render_row( + "Java", + None, + self.render_value("jdtls".to_string()), + )), + ), + ) + } + + fn render_ai_content(&self, _cx: &mut Context) -> impl IntoElement { + v_flex() + .w_full() + .gap_4() + .child( + self.render_group( + "模型", + v_flex() + .w_full() + .gap_3() + .child(self.render_row( + "提供方", + Some("来自默认设置,占位展示"), + self.render_value("anthropic".to_string()), + )) + .child(self.render_row( + "模型", + None, + self.render_value("claude-sonnet-4-6".to_string()), + )), + ), + ) + .child(self.render_group("说明", self.render_note("AI 助手配置将在后续版本接入。"))) + } + + fn render_ai_commit_content(&self, _cx: &mut Context) -> impl IntoElement { + v_flex().w_full().gap_4().child(self.render_group( + "提交信息生成", + v_flex().w_full().gap_3().child( + self.render_empty_state(IconName::Bot, "AI 提交信息生成尚未接入,后续版本提供"), + ), + )) + } + + fn render_git_content(&self, cx: &mut Context) -> impl IntoElement { + v_flex() + .w_full() + .gap_4() + .child( + self.render_group( + "状态", + v_flex() + .w_full() + .gap_3() + .child(self.render_row( + "自动刷新状态", + Some("占位项:Linux Settings 尚未提供该字段,暂不落盘"), + self.render_toggle( + "git-auto-refresh", + self.auto_refresh_git_status, + cx, + |this, cx| { + this.auto_refresh_git_status = !this.auto_refresh_git_status; + cx.notify(); + }, + ), + )) + .child(self.render_row( + "显示未跟踪文件", + None, + self.render_toggle( + "git-show-untracked", + self.show_untracked_files, + cx, + |this, cx| { + this.show_untracked_files = !this.show_untracked_files; + cx.notify(); + }, + ), + )) + .child(self.render_row( + "暂存区优先", + None, + self.render_toggle( + "git-show-staged-first", + self.show_staged_first, + cx, + |this, cx| { + this.show_staged_first = !this.show_staged_first; + cx.notify(); + }, + ), + )), + ), + ) + .child( + self.render_group( + "差异视图", + v_flex().w_full().gap_3().child( + self.render_row( + "默认视图", + None, + h_flex() + .gap_1p5() + .child(self.render_segment( + "diff-unified", + "统一", + self.git_default_diff_view == "unified", + cx, + |this, cx| { + this.git_default_diff_view = "unified".to_string(); + cx.notify(); + }, + )) + .child(self.render_segment( + "diff-split", + "分栏", + self.git_default_diff_view == "split", + cx, + |this, cx| { + this.git_default_diff_view = "split".to_string(); + cx.notify(); + }, + )), + ), + ), + ), + ) + .child(self.render_group( + "行内 Blame", + v_flex().w_full().gap_3().child(self.render_row( + "启用行内 Blame", + None, + self.render_toggle( + "git-inline-blame", + self.enable_inline_git_blame, + cx, + |this, cx| { + this.enable_inline_git_blame = !this.enable_inline_git_blame; + cx.notify(); + }, + ), + )), + )) + } + + fn render_logs_content(&self, cx: &mut Context) -> impl IntoElement { + v_flex() + .w_full() + .gap_4() + .child( + self.render_group( + "日志", + v_flex().w_full().gap_3().child( + self.render_row( + "日志级别", + Some("占位项:Linux Settings 尚未提供该字段,暂不落盘"), + h_flex() + .gap_1p5() + .child(self.render_segment( + "log-trace", + "TRACE", + self.log_level == "trace", + cx, + |this, cx| { + this.log_level = "trace".to_string(); + cx.notify(); + }, + )) + .child(self.render_segment( + "log-debug", + "DEBUG", + self.log_level == "debug", + cx, + |this, cx| { + this.log_level = "debug".to_string(); + cx.notify(); + }, + )) + .child(self.render_segment( + "log-info", + "INFO", + self.log_level == "info", + cx, + |this, cx| { + this.log_level = "info".to_string(); + cx.notify(); + }, + )) + .child(self.render_segment( + "log-warn", + "WARN", + self.log_level == "warn", + cx, + |this, cx| { + this.log_level = "warn".to_string(); + cx.notify(); + }, + )) + .child(self.render_segment( + "log-error", + "ERROR", + self.log_level == "error", + cx, + |this, cx| { + this.log_level = "error".to_string(); + cx.notify(); + }, + )), + ), + ), + ), + ) + .child(self.render_group("输出", self.render_note("日志会同时输出到终端与日志文件。"))) + } + + fn render_updates_content(&self, cx: &mut Context) -> impl IntoElement { + v_flex().w_full().gap_4().child( + self.render_group( + "软件更新", + v_flex().w_full().gap_3().child( + self.render_row( + "Lithe", + Some("当前版本为占位展示,检查更新尚未接入"), + Button::new("check-updates") + .small() + .primary() + .label("检查更新") + .on_click(cx.listener(|_this, _event, _window, cx| { + // 占位:仅刷新视图,不发起真实更新检查。 + cx.notify(); + })), + ), + ), + ), + ) } } diff --git a/linux/src/workbench/status_bar.rs b/linux/src/workbench/status_bar.rs index b9dd03a48..46cc6a94d 100644 --- a/linux/src/workbench/status_bar.rs +++ b/linux/src/workbench/status_bar.rs @@ -1,11 +1,30 @@ +//! 底部状态栏:复刻 Tauri `footer.tsx` / `footer-editor-status.tsx` / +//! `footer-status-chip.tsx`。 +//! +//! 左右两组项及其顺序完全由设置(`footer_leading_items_order`、 +//! `footer_trailing_items_order`)决定,与 Tauri `FOOTER_LEADING_ITEM_IDS` / +//! `FOOTER_TRAILING_ITEM_IDS` 对应。`gitChanges` chip 点击时只向外广播 +//! [`StatusBarEvent::OpenGit`],由 `view.rs` 打开 Git 侧边视图。 + use gpui_kit::assets::IconName; use gpui_kit::component::{h_flex, Icon}; use gpui_kit::{ - div, px, Context, FontWeight, IntoElement, ParentElement as _, Render, Styled as _, Window, + div, px, AnyElement, Context, EventEmitter, InteractiveElement as _, IntoElement, + ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Window, }; +use crate::settings; use crate::theme::ThemeColors; +/// 状态栏事件。 +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub enum StatusBarEvent { + /// 点击 gitChanges chip,请求打开 Git 视图。 + OpenGit, +} + +#[allow(dead_code)] pub struct StatusBarView { pub git_branch: Option, pub current_file: Option, @@ -13,8 +32,18 @@ pub struct StatusBarView { pub cursor_col: usize, pub language: String, pub encoding: String, + /// 缩进宽度(空格数),来自设置的 `tab_size`。 + pub indent_size: usize, + /// 当前编辑器是否为只读。 + pub read_only: bool, + /// 进程内存占用(MB)。 + pub memory_mb: usize, + /// 工作区变更文件数。 + pub git_changes: usize, } +impl EventEmitter for StatusBarView {} + impl StatusBarView { pub fn new() -> Self { Self { @@ -24,9 +53,14 @@ impl StatusBarView { cursor_col: 1, language: "Plain Text".to_string(), encoding: "UTF-8".to_string(), + indent_size: 4, + read_only: false, + memory_mb: 0, + git_changes: 0, } } + /// 编辑器状态更新入口。签名保持不变,`view.rs` 现有调用无需改动。 pub fn set_file_info( &mut self, file: Option, @@ -47,119 +81,180 @@ impl StatusBarView { self.git_branch = branch; cx.notify(); } + + #[allow(dead_code)] + pub fn set_git_changes(&mut self, count: usize, cx: &mut Context) { + self.git_changes = count; + cx.notify(); + } + + #[allow(dead_code)] + pub fn set_memory_mb(&mut self, mb: usize, cx: &mut Context) { + self.memory_mb = mb; + cx.notify(); + } +} + +impl Default for StatusBarView { + fn default() -> Self { + Self::new() + } +} + +/// 项之间的 1px 竖向分隔线。 +fn separator() -> AnyElement { + div() + .w(px(1.0)) + .h(px(10.0)) + .flex_shrink_0() + .bg(ThemeColors::border()) + .into_any_element() +} + +/// 带图标的标签:图标 + 文本共用同一颜色,避免两段颜色不一致。 +fn labeled(icon: IconName, label: String, color: gpui_kit::Rgba) -> AnyElement { + h_flex() + .items_center() + .gap_1() + .text_color(color) + .child(Icon::new(icon).size(px(12.0)).text_color(color)) + .child(div().child(label)) + .into_any_element() +} + +impl StatusBarView { + fn render_leading_item(&self, id: &str) -> Option { + match id { + "filePath" => { + let label = self + .current_file + .clone() + .unwrap_or_else(|| "No file active".to_string()); + Some(labeled( + IconName::FileText, + label, + ThemeColors::subtle_foreground(), + )) + } + // 分支未知时整项不渲染,避免用占位名误导用户。 + "branch" => self + .git_branch + .as_ref() + .map(|branch| labeled(IconName::GitBranch, branch.clone(), ThemeColors::success())), + _ => None, + } + } + + fn render_trailing_item(&self, id: &str, cx: &mut Context) -> Option { + match id { + "cursor" => Some( + div() + .child(format!("Ln {}, Col {}", self.cursor_line, self.cursor_col)) + .into_any_element(), + ), + "encoding" => Some(div().child(self.encoding.clone()).into_any_element()), + "indent" => Some( + div() + .child(format!("{} spaces", self.indent_size)) + .into_any_element(), + ), + "readOnly" => Some(labeled( + if self.read_only { + IconName::Lock + } else { + IconName::LockOpen + }, + if self.read_only { + "Read-only" + } else { + "Editable" + } + .to_string(), + ThemeColors::subtle_foreground(), + )), + "memory" => Some(labeled( + IconName::MemoryStick, + format!("{} MB", self.memory_mb), + ThemeColors::subtle_foreground(), + )), + "gitChanges" => Some(self.render_git_changes(cx)), + _ => None, + } + } + + fn render_git_changes(&self, cx: &mut Context) -> AnyElement { + if self.git_changes == 0 { + // 无变更时用成功色对勾,和 Tauri 的 CheckCircleIcon text-success 对齐。 + labeled( + IconName::CircleCheck, + "0".to_string(), + ThemeColors::success(), + ) + } else { + let count = self.git_changes; + h_flex() + .id("status-git-changes") + .items_center() + .gap_1() + .cursor_pointer() + .text_color(ThemeColors::git_modified()) + .hover(|h| h.bg(ThemeColors::accent())) + .child(div().child(count.to_string())) + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(StatusBarEvent::OpenGit); + })) + .into_any_element() + } + } } impl Render for StatusBarView { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let branch = self.git_branch.clone().unwrap_or_else(|| "main".to_string()); - let file_label = self - .current_file - .clone() - .unwrap_or_else(|| "No file active".to_string()); + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + // 只读快照顺序,随后释放对全局设置的借用。 + let (leading_order, trailing_order) = { + let s = settings::get(cx); + ( + s.footer_leading_items_order.clone(), + s.footer_trailing_items_order.clone(), + ) + }; + + let leading = join_items( + leading_order + .iter() + .filter_map(|id| self.render_leading_item(id)), + ); + let trailing = join_items( + trailing_order + .iter() + .filter_map(|id| self.render_trailing_item(id, cx)), + ); h_flex() .h(px(24.0)) .w_full() - .bg(ThemeColors::bg_statusbar()) + .flex_shrink_0() + .bg(ThemeColors::background()) .border_t_1() .border_color(ThemeColors::border()) .items_center() .justify_between() .px_3() .text_xs() - .text_color(ThemeColors::text_muted()) - .child( - // 左侧状态栏项目(分支 + 文件状态) - h_flex() - .items_center() - .gap_2() - .child( - h_flex() - .items_center() - .gap_1p5() - .text_color(ThemeColors::accent_green()) - .child( - Icon::new(IconName::GitBranch) - .size(px(12.0)) - .text_color(ThemeColors::accent_green()), - ) - .child( - div() - .font_weight(FontWeight::SEMIBOLD) - .child(branch), - ), - ) - .child( - div() - .h(px(10.0)) - .w(px(1.0)) - .bg(ThemeColors::border()) - .mx_1(), - ) - .child( - h_flex() - .items_center() - .gap_1p5() - .child( - Icon::new(IconName::FileText) - .size(px(12.0)) - .text_color(ThemeColors::text_muted()), - ) - .child( - div() - .text_color(ThemeColors::text_primary()) - .child(file_label), - ), - ), - ) - .child( - // 右侧状态栏项目(行列号、缩进、编码、语言、核心状态) - h_flex() - .items_center() - .gap_2() - .child(format!("Ln {}, Col {}", self.cursor_line, self.cursor_col)) - .child( - div() - .h(px(10.0)) - .w(px(1.0)) - .bg(ThemeColors::border()), - ) - .child("4 spaces") - .child( - div() - .h(px(10.0)) - .w(px(1.0)) - .bg(ThemeColors::border()), - ) - .child(self.encoding.clone()) - .child( - div() - .h(px(10.0)) - .w(px(1.0)) - .bg(ThemeColors::border()), - ) - .child(self.language.clone()) - .child( - div() - .h(px(10.0)) - .w(px(1.0)) - .bg(ThemeColors::border()), - ) - .child( - h_flex() - .items_center() - .gap_1() - .child( - Icon::new(IconName::Check) - .size(px(12.0)) - .text_color(ThemeColors::accent_green()), - ) - .child( - div() - .text_color(ThemeColors::accent_blue()) - .child("Lithe Core"), - ), - ), - ) + .text_color(ThemeColors::subtle_foreground()) + .child(h_flex().items_center().gap_2().min_w_0().children(leading)) + .child(h_flex().items_center().gap_2().children(trailing)) + } +} + +/// 用分隔线拼接一组项。 +fn join_items(items: impl Iterator) -> Vec { + let mut out: Vec = Vec::new(); + for item in items { + if !out.is_empty() { + out.push(separator()); + } + out.push(item); } + out } diff --git a/linux/src/workbench/toolbar.rs b/linux/src/workbench/toolbar.rs index 781991340..78fc10da8 100644 --- a/linux/src/workbench/toolbar.rs +++ b/linux/src/workbench/toolbar.rs @@ -1,17 +1,25 @@ +//! 顶部标题栏/工具栏:复刻 Tauri `title-bar.tsx` + `window-menu-bar.tsx`。 +//! +//! 结构从左到右:应用菜单栏(紧凑或九宫格)、品牌、项目胶囊、分支胶囊、 +//! 居中全局搜索条、运行目标胶囊、运行/调试/停止、主题与设置、窗口控件。 +//! 所有交互只通过 [`ToolbarEvent`] 向外广播,具体业务由 `view.rs` 订阅处理。 + use gpui_kit::assets::IconName; use gpui_kit::component::button::{Button, ButtonVariants as _}; -use gpui_kit::component::menu::{DropdownMenu as _, PopupMenuItem}; +use gpui_kit::component::menu::{DropdownMenu as _, PopupMenu, PopupMenuItem}; use gpui_kit::component::{h_flex, Icon, Sizable as _}; use gpui_kit::{ - div, px, Context, EventEmitter, FontWeight, InteractiveElement as _, IntoElement, - ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Window, + div, px, AnyElement, Context, EventEmitter, FontWeight, InteractiveElement as _, IntoElement, + ParentElement as _, Render, SharedString, StatefulInteractiveElement as _, Styled as _, Window, }; +use crate::settings; use crate::theme::ThemeColors; #[derive(Debug, Clone)] #[allow(dead_code)] pub enum ToolbarEvent { + // ---- 旧事件(保留以兼容 view.rs 现有订阅) ---- NewFile, Save, CloseTab, @@ -26,6 +34,19 @@ pub enum ToolbarEvent { OpenSettings, About, Exit, + // ---- 新增:菜单项统一出口 ---- + /// 任意应用菜单项被点击,载荷为稳定 id(如 `file.new_file`)。 + MenuAction(String), + /// 切换浅色/深色主题。 + ToggleTheme, + WindowMinimize, + WindowMaximize, + WindowClose, + // ---- 新增:项目胶囊动作 ---- + NewProject, + OpenProject, + CloneRepository, + OpenRecent, } pub struct ToolbarView { @@ -54,180 +75,383 @@ impl ToolbarView { } } +/// 单个菜单项:显示文本 + 稳定动作 id。 +struct MenuEntry { + label: &'static str, + action: &'static str, +} + +/// 便于在 `const` 菜单表中声明条目。 +const fn entry(label: &'static str, action: &'static str) -> MenuEntry { + MenuEntry { label, action } +} + +/// 一个顶层应用菜单:标题 + 分组(分组之间渲染分隔线)。 +struct AppMenu { + title: &'static str, + groups: &'static [&'static [MenuEntry]], +} + +// ---- 九个应用菜单的静态定义,与 Tauri `window-menu-bar.tsx` 对齐 ---- +const FILE_MENU: &[&[MenuEntry]] = &[ + &[ + entry("New File", "file.new_file"), + entry("New Window", "file.new_window"), + ], + &[ + entry("Open File...", "file.open_file"), + entry("Open Folder...", "file.open_folder"), + ], + &[ + entry("Save", "file.save"), + entry("Save All", "file.save_all"), + ], + &[ + entry("Close Editor", "file.close_editor"), + entry("Close Window", "file.close_window"), + ], + &[entry("Exit", "file.exit")], +]; + +const EDIT_MENU: &[&[MenuEntry]] = &[ + &[entry("Undo", "edit.undo"), entry("Redo", "edit.redo")], + &[ + entry("Cut", "edit.cut"), + entry("Copy", "edit.copy"), + entry("Paste", "edit.paste"), + ], + &[entry("Find", "edit.find"), entry("Replace", "edit.replace")], +]; + +const VIEW_MENU: &[&[MenuEntry]] = &[ + &[ + entry("Toggle Sidebar", "view.toggle_sidebar"), + entry("Toggle Bottom Panel", "view.toggle_bottom_panel"), + entry("Toggle Status Bar", "view.toggle_status_bar"), + ], + &[ + entry("Command Palette", "view.command_palette"), + entry("Quick Open", "view.quick_open"), + ], + &[ + entry("Zoom In", "view.zoom_in"), + entry("Zoom Out", "view.zoom_out"), + entry("Reset Zoom", "view.reset_zoom"), + ], +]; + +const GO_MENU: &[&[MenuEntry]] = &[ + &[entry("Back", "go.back"), entry("Forward", "go.forward")], + &[ + entry("Go to File", "go.go_to_file"), + entry("Go to Symbol", "go.go_to_symbol"), + entry("Go to Line", "go.go_to_line"), + ], +]; + +const TERMINAL_MENU: &[&[MenuEntry]] = &[&[ + entry("New Terminal", "terminal.new"), + entry("Split Terminal", "terminal.split"), + entry("Clear Terminal", "terminal.clear"), +]]; + +const RUN_MENU: &[&[MenuEntry]] = &[ + &[ + entry("Run", "run.run"), + entry("Debug", "run.debug"), + entry("Stop", "run.stop"), + ], + &[entry("Run Without Debugging", "run.run_without_debugging")], +]; + +const TOOLS_MENU: &[&[MenuEntry]] = &[ + &[ + entry("Settings", "tools.settings"), + entry("Extensions", "tools.extensions"), + ], + &[ + entry("Database", "tools.database"), + entry("Diagnostics", "tools.diagnostics"), + ], +]; + +const WINDOW_MENU: &[&[MenuEntry]] = &[&[ + entry("Minimize", "window.minimize"), + entry("Maximize", "window.maximize"), + entry("Close", "window.close"), +]]; + +const HELP_MENU: &[&[MenuEntry]] = &[ + &[ + entry("Documentation", "help.documentation"), + entry("Keyboard Shortcuts", "help.keyboard_shortcuts"), + ], + &[entry("About Lithe", "help.about")], +]; + +const APP_MENUS: &[AppMenu] = &[ + AppMenu { + title: "File", + groups: FILE_MENU, + }, + AppMenu { + title: "Edit", + groups: EDIT_MENU, + }, + AppMenu { + title: "View", + groups: VIEW_MENU, + }, + AppMenu { + title: "Go", + groups: GO_MENU, + }, + AppMenu { + title: "Terminal", + groups: TERMINAL_MENU, + }, + AppMenu { + title: "Run", + groups: RUN_MENU, + }, + AppMenu { + title: "Tools", + groups: TOOLS_MENU, + }, + AppMenu { + title: "Window", + groups: WINDOW_MENU, + }, + AppMenu { + title: "Help", + groups: HELP_MENU, + }, +]; + +/// 构造一个发出 `MenuAction(id)` 的菜单项。 +fn menu_action_item( + label: impl Into, + action: &'static str, + view: &gpui_kit::Entity, +) -> PopupMenuItem { + let v = view.clone(); + PopupMenuItem::new(label).on_click(move |_, _, cx| { + v.update(cx, |_, cx| { + cx.emit(ToolbarEvent::MenuAction(action.to_string())) + }); + }) +} + +/// 构造一个直接发出指定事件的菜单项(用于项目胶囊等固定动作)。 +fn event_item( + label: impl Into, + make: fn() -> ToolbarEvent, + view: &gpui_kit::Entity, +) -> PopupMenuItem { + let v = view.clone(); + PopupMenuItem::new(label).on_click(move |_, _, cx| { + let event = make(); + v.update(cx, |_, cx| cx.emit(event)); + }) +} + +/// 按分组把菜单项灌入 `PopupMenu`,分组之间自动插入分隔线。 +fn build_menu( + mut menu: PopupMenu, + groups: &[&[MenuEntry]], + view: &gpui_kit::Entity, +) -> PopupMenu { + let mut first_group = true; + for group in groups { + if !first_group { + menu = menu.separator(); + } + first_group = false; + for item in *group { + menu = menu.item(menu_action_item(item.label, item.action, view)); + } + } + menu +} + impl Render for ToolbarView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let branch = self.git_branch.clone().unwrap_or_else(|| "main".to_string()); + let branch = self + .git_branch + .clone() + .unwrap_or_else(|| "main".to_string()); + let project_name = self.workspace_name.clone(); + + // 读取渲染所需的设置快照,随后立刻释放对 cx 的只读借用。 + let (compact_menu, theme_icon) = { + let s = settings::get(cx); + let icon = if s.sync_system_theme { + IconName::Moon + } else if s.theme == "lithe-light" { + IconName::Sun + } else { + IconName::Moon + }; + (s.compact_menu_bar, icon) + }; + let view = cx.entity(); + // 应用菜单栏:紧凑模式为单个 Menu 按钮 + 九个子菜单;否则平铺九个顶层菜单。 + let app_menu: AnyElement = if compact_menu { + let v = view.clone(); + Button::new("tb-app-menu") + .small() + .ghost() + .icon(IconName::Menu) + .tooltip("Menu") + .dropdown_menu(move |menu, window, cx| { + let mut menu = menu; + for app in APP_MENUS { + let v_sub = v.clone(); + menu = menu.submenu(app.title, window, cx, move |sub, _window, _cx| { + build_menu(sub, app.groups, &v_sub) + }); + } + menu + }) + .into_any_element() + } else { + h_flex() + .items_center() + .children(APP_MENUS.iter().map(|app| { + let v = view.clone(); + Button::new(format!("tb-menu-{}", app.title)) + .small() + .ghost() + .label(app.title) + .dropdown_menu(move |menu, _window, _cx| build_menu(menu, app.groups, &v)) + })) + .into_any_element() + }; + h_flex() .h(px(40.0)) .w_full() - .bg(ThemeColors::bg_titlebar()) + .bg(ThemeColors::background()) .border_b_1() .border_color(ThemeColors::border()) .items_center() - .justify_between() - .px_3() + .pl_3() + // 1. 左侧:应用菜单 + 品牌 + 项目胶囊 + 分支胶囊 .child( - // 1. 左侧:Lithe Logo + 项目选择器胶囊 + 分支选择器胶囊 h_flex() + .flex_1() + .min_w_0() .items_center() - .gap_3() + .gap_2() + .child(app_menu) .child( - // Lithe Logo + 品牌名 + // Lithe 品牌标识 h_flex() .items_center() .gap_1p5() - .pr_1() + .pl_1() .child( Icon::new(IconName::Zap) .size(px(16.0)) - .text_color(ThemeColors::accent_blue()), + .text_color(ThemeColors::primary()), ) .child( div() .font_weight(FontWeight::BOLD) .text_sm() - .text_color(ThemeColors::text_primary()) + .text_color(ThemeColors::foreground()) .child("Lithe"), ), ) - // 项目选择器胶囊(点击可展开项目切换下拉菜单) .child({ + // 项目选择器胶囊:Folder 图标 + 项目名 + ChevronDown,点击展开项目菜单 let v = view.clone(); + let name = project_name.clone(); Button::new("tb-project-selector") .small() .ghost() - .icon(IconName::Folder) - .label(self.workspace_name.clone()) - .bg(ThemeColors::bg_tab_hover()) + .rounded_md() + .bg(ThemeColors::surface()) .border_1() .border_color(ThemeColors::border()) - .rounded_md() + .child( + Icon::new(IconName::Folder) + .size(px(13.0)) + .text_color(ThemeColors::muted_foreground()), + ) + .child( + div() + .text_xs() + .text_color(ThemeColors::foreground()) + .child(name.clone()), + ) + .child( + Icon::new(IconName::ChevronDown) + .size(px(12.0)) + .text_color(ThemeColors::subtle_foreground()), + ) .dropdown_menu(move |menu, _window, _cx| { - let v_refresh = v.clone(); - let v_new_file = v.clone(); - let v_save = v.clone(); - let v_close = v.clone(); - let v_sidebar = v.clone(); - let v_terminal = v.clone(); - let v_settings = v.clone(); - let v_about = v.clone(); - let v_exit = v.clone(); - - menu.item( - PopupMenuItem::new("New File (Ctrl+N)") - .on_click(move |_, _, cx| { - v_new_file.update(cx, |_, cx| { - cx.emit(ToolbarEvent::NewFile) - }); - }), - ) - .item( - PopupMenuItem::new("Save (Ctrl+S)") - .on_click(move |_, _, cx| { - v_save.update(cx, |_, cx| { - cx.emit(ToolbarEvent::Save) - }); - }), - ) - .item( - PopupMenuItem::new("Close Active Tab (Ctrl+W)") - .on_click(move |_, _, cx| { - v_close.update(cx, |_, cx| { - cx.emit(ToolbarEvent::CloseTab) - }); - }), - ) + menu.item(event_item( + "New Project", + || ToolbarEvent::NewProject, + &v, + )) + .item(event_item("Open Project", || ToolbarEvent::OpenProject, &v)) + .item(event_item( + "Clone Repository", + || ToolbarEvent::CloneRepository, + &v, + )) + .item(event_item("Open Recent", || ToolbarEvent::OpenRecent, &v)) + .item(event_item( + "Open Projects", + || ToolbarEvent::OpenProject, + &v, + )) .separator() - .item( - PopupMenuItem::new("Toggle Sidebar (Ctrl+B)") - .on_click(move |_, _, cx| { - v_sidebar.update(cx, |_, cx| { - cx.emit(ToolbarEvent::ToggleSidebar) - }); - }), - ) - .item( - PopupMenuItem::new("Toggle Terminal (Ctrl+`)") - .on_click(move |_, _, cx| { - v_terminal.update(cx, |_, cx| { - cx.emit(ToolbarEvent::ToggleTerminal) - }); - }), - ) - .item( - PopupMenuItem::new("Refresh Workspace") - .on_click(move |_, _, cx| { - v_refresh.update(cx, |_, cx| { - cx.emit(ToolbarEvent::RefreshWorkspace) - }); - }), - ) - .separator() - .item( - PopupMenuItem::new("Settings") - .on_click(move |_, _, cx| { - v_settings.update(cx, |_, cx| { - cx.emit(ToolbarEvent::OpenSettings) - }); - }), - ) - .item( - PopupMenuItem::new("About Lithe") - .on_click(move |_, _, cx| { - v_about.update(cx, |_, cx| { - cx.emit(ToolbarEvent::About) - }); - }), - ) - .separator() - .item( - PopupMenuItem::new("Exit") - .on_click(move |_, _, cx| { - v_exit.update(cx, |_, cx| { - cx.emit(ToolbarEvent::Exit) - }); - }), - ) + // 当前项目打勾标记,仅展示 + .item(PopupMenuItem::new(name.clone()).icon(IconName::CircleCheck)) }) }) - // 分支选择器胶囊:带 GitBranch 图标、分支名和 ChevronDown .child( + // 分支胶囊:只展示当前分支,不接真实切换 h_flex() .items_center() .gap_1p5() .px_2() .py(px(3.0)) .rounded_md() - .bg(ThemeColors::bg_tab_hover()) + .bg(ThemeColors::surface()) .border_1() .border_color(ThemeColors::border()) .child( Icon::new(IconName::GitBranch) .size(px(13.0)) - .text_color(ThemeColors::accent_green()), + .text_color(ThemeColors::success()), ) .child( div() .text_xs() - .text_color(ThemeColors::accent_green()) + .text_color(ThemeColors::success()) .child(branch), ) .child( Icon::new(IconName::ChevronDown) .size(px(12.0)) - .text_color(ThemeColors::text_muted()), + .text_color(ThemeColors::subtle_foreground()), ), ), ) + // 2. 中间:全局搜索条 .child( - // 2. 中间:全局搜索栏 (Search Everywhere) div() .id("search-everywhere-bar") .w(px(280.0)) .h(px(28.0)) - .bg(ThemeColors::bg_tab_active()) + .flex_shrink_0() + .bg(ThemeColors::surface()) .border_1() .border_color(ThemeColors::border()) .rounded(px(6.0)) @@ -236,7 +460,7 @@ impl Render for ToolbarView { .items_center() .justify_between() .cursor_pointer() - .hover(|h| h.border_color(ThemeColors::accent_blue())) + .hover(|h| h.border_color(ThemeColors::primary())) .on_click(cx.listener(|_this, _event, _window, cx| { cx.emit(ToolbarEvent::QuickOpen); })) @@ -247,32 +471,35 @@ impl Render for ToolbarView { .child( Icon::new(IconName::Search) .size(px(13.0)) - .text_color(ThemeColors::text_muted()), + .text_color(ThemeColors::subtle_foreground()), ) .child( div() .text_xs() - .text_color(ThemeColors::text_muted()) + .text_color(ThemeColors::subtle_foreground()) .child("Search files, symbols..."), ), ) .child( div() .text_xs() - .text_color(ThemeColors::text_muted()) + .text_color(ThemeColors::subtle_foreground()) .px_1p5() .py(px(1.0)) - .bg(ThemeColors::bg_titlebar()) + .bg(ThemeColors::background()) .border_1() .border_color(ThemeColors::border()) .rounded(px(4.0)) .child("Ctrl+P"), ), ) + // 3. 右侧工具组:运行目标、运行/调试/停止、主题、设置 .child( - // 3. 右侧:运行目标胶囊 + 运行/调试/停止/设置控制组 h_flex() + .flex_1() + .min_w_0() .items_center() + .justify_end() .gap_2() .child( // 运行目标胶囊 @@ -282,19 +509,19 @@ impl Render for ToolbarView { .px_2() .py(px(3.0)) .rounded_md() - .bg(ThemeColors::bg_tab_hover()) + .bg(ThemeColors::surface()) .border_1() .border_color(ThemeColors::border()) .child( div() .text_xs() - .text_color(ThemeColors::text_primary()) + .text_color(ThemeColors::foreground()) .child("[Project] Default"), ) .child( Icon::new(IconName::ChevronDown) .size(px(12.0)) - .text_color(ThemeColors::text_muted()), + .text_color(ThemeColors::subtle_foreground()), ), ) .child( @@ -327,6 +554,17 @@ impl Render for ToolbarView { cx.emit(ToolbarEvent::Stop); })), ) + .child( + // 主题切换:跟随系统时恒显示 Moon,否则按当前主题显示 Sun/Moon + Button::new("tb-theme-toggle") + .small() + .ghost() + .icon(theme_icon) + .tooltip("Toggle Theme") + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(ToolbarEvent::ToggleTheme); + })), + ) .child( Button::new("tb-settings") .small() @@ -338,5 +576,64 @@ impl Render for ToolbarView { })), ), ) + // 4. 窗口控件:46x40 直角按钮,Close 悬停使用 destructive + .child( + h_flex() + .items_center() + .flex_shrink_0() + .child(window_control( + "tb-window-minimize", + IconName::Minus, + false, + cx.listener(|_this, _event: &gpui_kit::ClickEvent, _window, cx| { + cx.emit(ToolbarEvent::WindowMinimize); + }), + )) + .child(window_control( + "tb-window-maximize", + IconName::Square, + false, + cx.listener(|_this, _event: &gpui_kit::ClickEvent, _window, cx| { + cx.emit(ToolbarEvent::WindowMaximize); + }), + )) + .child(window_control( + "tb-window-close", + IconName::Close, + true, + cx.listener(|_this, _event: &gpui_kit::ClickEvent, _window, cx| { + cx.emit(ToolbarEvent::WindowClose); + }), + )), + ) } } + +/// 构建一个窗口控件按钮:固定 46x40、直角,悬停高亮,Close 使用 destructive。 +fn window_control( + id: &'static str, + icon: IconName, + destructive: bool, + on_click: impl Fn(&gpui_kit::ClickEvent, &mut Window, &mut gpui_kit::App) + 'static, +) -> impl IntoElement { + let hover_bg = if destructive { + ThemeColors::destructive() + } else { + ThemeColors::accent() + }; + div() + .id(id) + .w(px(46.0)) + .h(px(40.0)) + .flex() + .items_center() + .justify_center() + .cursor_pointer() + .hover(move |h| h.bg(hover_bg)) + .on_click(on_click) + .child( + Icon::new(icon) + .size(px(14.0)) + .text_color(ThemeColors::muted_foreground()), + ) +} diff --git a/linux/src/workbench/view.rs b/linux/src/workbench/view.rs index 2a7b2950c..727c4ffdb 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -1,3 +1,8 @@ +//! 工作台集成层:装配各区域视图、订阅事件、处理快捷键与布局调整。 +//! +//! 结构对齐 Tauri `MainLayout`:顶栏 / 多项目标签条 / 活动栏 + 侧边栏 + 编辑区 + +//! 右侧插件活动栏 / 底部面板 / 状态栏;无项目时显示欢迎页,浮层由模态状态控制。 + use gpui_kit::component::{h_flex, v_flex}; use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::{ @@ -7,27 +12,31 @@ use gpui_kit::{ }; use crate::core::CoreClient; +use crate::settings; use crate::theme::ThemeColors; -use crate::workbench::activity_rail::{ActivityRailEvent, ActivityRailView, ActivityTab}; -use crate::workbench::bottom_panel::BottomPanelView; -use crate::workbench::editor::EditorView; -use crate::workbench::search_everywhere::{ - SearchEverywhereEvent, SearchEverywhereModal, +use crate::workbench::activity_rail::{ + ActivityRailEvent, ActivityRailView, PluginActivityRailView, PluginRailEvent, }; +use crate::workbench::bottom_panel::{BottomPanelView, BottomTab}; +use crate::workbench::command_palette::{CommandPaletteEvent, CommandPaletteModal}; +use crate::workbench::editor::EditorView; +use crate::workbench::quick_open::{QuickOpenEvent, QuickOpenModal}; +use crate::workbench::search_everywhere::{SearchEverywhereEvent, SearchEverywhereModal}; use crate::workbench::settings_dialog::{SettingsDialog, SettingsEvent}; use crate::workbench::sidebar::{FileEntry, SidebarEvent, SidebarTab, SidebarView}; -use crate::workbench::status_bar::StatusBarView; +use crate::workbench::status_bar::{StatusBarEvent, StatusBarView}; use crate::workbench::toolbar::{ToolbarEvent, ToolbarView}; +use crate::workbench::welcome_screen::{WelcomeEvent, WelcomeScreenView}; -/// Linux 前端主工作台视图组件(复刻 macOS LitheTheme / IntelliJ 架构) +/// 主工作台视图。 pub struct WorkbenchView { /// 项目工作区根路径 pub workspace_root: String, /// 侧边栏是否展开 pub sidebar_visible: bool, - /// 侧边栏宽度(默认 260.0,支持拖拽调节) + /// 侧边栏宽度(支持拖拽调节) pub sidebar_width: f32, - /// 底部面板高度(默认 240.0,支持拖拽调节) + /// 底部面板高度(支持拖拽调节) pub bottom_panel_height: f32, /// 是否正在调节侧边栏宽度 pub is_resizing_sidebar: bool, @@ -37,8 +46,14 @@ pub struct WorkbenchView { drag_start_y: f32, initial_resize_size: f32, + /// 是否显示欢迎页(无打开项目时) + pub show_welcome: bool, /// 全局搜索弹窗浮层是否可见 pub show_search_everywhere: bool, + /// 快速打开浮层是否可见 + pub show_quick_open: bool, + /// 命令面板浮层是否可见 + pub show_command_palette: bool, /// 设置模态对话框是否可见 pub show_settings_dialog: bool, @@ -46,6 +61,8 @@ pub struct WorkbenchView { pub toolbar: Entity, /// 左侧垂直活动栏 (Activity Rail) pub activity_rail: Entity, + /// 右侧插件活动栏 (Extensions / Notifications / Maven) + pub plugin_rail: Entity, /// 侧边栏面板 (Files / Git / Search) pub sidebar: Entity, /// 主代码编辑器区 @@ -56,8 +73,14 @@ pub struct WorkbenchView { pub status_bar: Entity, /// 全局搜索弹窗 pub search_everywhere: Entity, + /// 快速打开弹窗 + pub quick_open: Entity, + /// 命令面板 + pub command_palette: Entity, /// 设置模态对话框 pub settings_dialog: Entity, + /// 欢迎页 + pub welcome_screen: Entity, focus_handle: FocusHandle, client: CoreClient, @@ -73,73 +96,77 @@ impl WorkbenchView { Self::with_root(workspace_root, window, cx) } - pub fn with_root( - workspace_root: String, - window: &mut Window, - cx: &mut Context, - ) -> Self { + pub fn with_root(workspace_root: String, window: &mut Window, cx: &mut Context) -> Self { let root = workspace_root.clone(); let toolbar = cx.new(|_cx| ToolbarView::new(&root)); - let activity_rail = cx.new(|_cx| ActivityRailView::new()); + let activity_rail = cx.new(|cx| ActivityRailView::new(cx)); + let plugin_rail = cx.new(|_cx| PluginActivityRailView::new()); let sidebar = cx.new(|cx| SidebarView::new(root.clone(), cx)); let editor = cx.new(|cx| EditorView::new(root.clone(), window, cx)); let bottom_panel = cx.new(|cx| BottomPanelView::new(root.clone(), cx)); let status_bar = cx.new(|_cx| StatusBarView::new()); let search_everywhere = cx.new(|cx| SearchEverywhereModal::new(cx)); - let settings_dialog = cx.new(|_cx| SettingsDialog::new()); + let quick_open = cx.new(|cx| QuickOpenModal::new(cx)); + let command_palette = cx.new(|cx| CommandPaletteModal::new(cx)); + let settings_dialog = cx.new(|cx| SettingsDialog::new(cx)); + let welcome_screen = cx.new(|cx| WelcomeScreenView::new(cx)); let focus_handle = cx.focus_handle(); // 1. 订阅侧边栏事件(打开文件、新建文件、提交 Git) let status_bar_clone = status_bar.clone(); let bottom_panel_sidebar = bottom_panel.clone(); - let sub_sidebar = cx.subscribe(&sidebar, move |this, sidebar, event: &SidebarEvent, cx| { - match event { - SidebarEvent::OpenFile(path) => { - this.open_file(path, cx); - let _ = status_bar_clone.update(cx, |sb, cx| { - sb.set_file_info( - Some(path.clone()), - 1, - 1, - EditorView::language_name(path).to_string(), - cx, - ); - }); - } - SidebarEvent::NewFile => { - let _ = this.editor.update(cx, |ed, cx| { - ed.open_file("untitled.txt".to_string(), String::new(), cx); - }); - let _ = status_bar_clone.update(cx, |sb, cx| { - sb.set_file_info( - Some("untitled.txt".to_string()), - 1, - 1, - "Plain Text".to_string(), - cx, - ); - }); - } - SidebarEvent::Commit(msg) => { - let log_msg = format!("[Git Commit] {}", msg); - let _ = bottom_panel_sidebar.update(cx, |bp, cx| { - bp.append_log(log_msg.clone(), cx); - let _ = bp.terminal.update(cx, |term, cx| { - term.send_command(&format!("git commit -m \"{}\"", msg), cx); + let sub_sidebar = + cx.subscribe( + &sidebar, + move |this, sidebar, event: &SidebarEvent, cx| match event { + SidebarEvent::OpenFile(path) => { + this.open_file(path, cx); + let _ = status_bar_clone.update(cx, |sb, cx| { + sb.set_file_info( + Some(path.clone()), + 1, + 1, + EditorView::language_name(path).to_string(), + cx, + ); }); - }); - let _ = sidebar.update(cx, |sb, cx| { - sb.refresh_git(cx); - }); - } - } - }); + } + SidebarEvent::NewFile => { + let _ = this.editor.update(cx, |ed, cx| { + ed.open_file("untitled.txt".to_string(), String::new(), cx); + }); + let _ = status_bar_clone.update(cx, |sb, cx| { + sb.set_file_info( + Some("untitled.txt".to_string()), + 1, + 1, + "Plain Text".to_string(), + cx, + ); + }); + } + SidebarEvent::Commit(msg) => { + let log_msg = format!("[Git Commit] {}", msg); + let _ = bottom_panel_sidebar.update(cx, |bp, cx| { + bp.append_log(log_msg.clone(), cx); + let _ = bp.terminal.update(cx, |term, cx| { + term.send_command(&format!("git commit -m \"{}\"", msg), cx); + }); + }); + let _ = sidebar.update(cx, |sb, cx| { + sb.refresh_git(cx); + }); + } + }, + ); - // 2. 观察侧边栏状态变化(分支同步与工作区文件快照同步给全局搜索) + // 2. 观察侧边栏状态变化(分支同步、文件快照同步给全局搜索与快速打开) let toolbar_branch_sync = toolbar.clone(); let status_bar_branch_sync = status_bar.clone(); let search_sync = search_everywhere.clone(); + let quick_open_sync = quick_open.clone(); + let status_bar_git_sync = status_bar.clone(); let obs_sidebar = cx.observe(&sidebar, move |_this, sidebar, cx| { let branch = sidebar.read(cx).git_branch.clone(); let _ = toolbar_branch_sync.update(cx, |tb, cx| { @@ -148,144 +175,206 @@ impl WorkbenchView { let _ = status_bar_branch_sync.update(cx, |sb, cx| { sb.set_git_branch(branch, cx); }); + let _ = status_bar_git_sync.update(cx, |sb, cx| { + sb.set_git_changes(sidebar.read(cx).git_changes.len(), cx); + }); - // 收集所有文件供全局搜索 + // 收集所有文件供全局搜索与快速打开 let mut file_list = Vec::new(); if let Some(root_node) = &sidebar.read(cx).root_node { Self::collect_all_file_paths(root_node, &mut file_list); } if !file_list.is_empty() { let _ = search_sync.update(cx, |search, cx| { - search.set_files(file_list, cx); + search.set_files(file_list.clone(), cx); + }); + let _ = quick_open_sync.update(cx, |qo, cx| { + qo.set_files(file_list, cx); }); } }); - // 3. 订阅左侧活动栏 (Activity Rail) 事件 + // 3. 订阅左侧活动栏事件 let sidebar_clone = sidebar.clone(); let bottom_panel_clone = bottom_panel.clone(); - let sub_rail = cx.subscribe(&activity_rail, move |this, rail, event: &ActivityRailEvent, cx| { - match event { - ActivityRailEvent::SelectTab(tab) => { - let sb_tab = match tab { - ActivityTab::Explorer => SidebarTab::Explorer, - ActivityTab::Git => SidebarTab::Git, - ActivityTab::Search => SidebarTab::Search, - }; - + let sub_rail = cx.subscribe( + &activity_rail, + move |this, rail, event: &ActivityRailEvent, cx| match event { + ActivityRailEvent::SelectView(view_id) => { let was_active = this.sidebar_visible - && rail.read(cx).active_tab == Some(*tab); + && rail.read(cx).active_view.as_deref() == Some(view_id); if was_active { this.sidebar_visible = false; - let _ = rail.update(cx, |r, cx| r.set_active_tab(None, cx)); + let _ = rail.update(cx, |r, cx| r.set_active_view(None, cx)); } else { this.sidebar_visible = true; - let _ = rail.update(cx, |r, cx| r.set_active_tab(Some(*tab), cx)); - let _ = sidebar_clone.update(cx, |sb, cx| sb.set_tab(sb_tab, cx)); + let _ = + rail.update(cx, |r, cx| r.set_active_view(Some(view_id.clone()), cx)); + if let Some(tab) = sidebar_tab_for(view_id) { + let _ = sidebar_clone.update(cx, |sb, cx| sb.set_tab(tab, cx)); + } } cx.notify(); } - ActivityRailEvent::ToggleTerminal => { - let _ = bottom_panel_clone.update(cx, |bp, cx| { - bp.toggle_collapsed(cx); - }); + ActivityRailEvent::ToggleBottomPane(pane_id) => { + let target = bottom_tab_for(pane_id); + let collapsed = bottom_panel_clone.read(cx).is_collapsed; + let already = !collapsed + && target.is_some() + && bottom_panel_clone.read(cx).active_tab == target.unwrap(); + if already { + let _ = bottom_panel_clone.update(cx, |bp, cx| bp.toggle_collapsed(cx)); + let _ = rail.update(cx, |r, cx| r.set_active_bottom(None, cx)); + } else { + if let Some(tab) = target { + let _ = bottom_panel_clone.update(cx, |bp, cx| bp.set_tab(tab, cx)); + } + let _ = + rail.update(cx, |r, cx| r.set_active_bottom(Some(pane_id.clone()), cx)); + } + cx.notify(); } ActivityRailEvent::OpenSettings => { - this.show_settings_dialog = true; - cx.notify(); + this.open_settings(cx); } - } - }); + }, + ); - // 4. 订阅顶部工具栏事件 - let sub_toolbar = cx.subscribe(&toolbar, |this, _toolbar, event: &ToolbarEvent, cx| { - match event { - ToolbarEvent::NewFile => { - let _ = this.editor.update(cx, |ed, cx| { - ed.open_file("untitled.txt".to_string(), String::new(), cx); - }); - } - ToolbarEvent::Save => { - let _ = this.editor.update(cx, |ed, cx| { - ed.save_active(cx); - }); - } - ToolbarEvent::CloseTab => { - let _ = this.editor.update(cx, |ed, cx| { - if let Some(idx) = ed.active_tab_index { - ed.close_tab(idx, cx); - } - }); + // 4. 订阅右侧插件活动栏事件 + let sub_plugin_rail = cx.subscribe( + &plugin_rail, + |this, _rail, event: &PluginRailEvent, cx| match event { + PluginRailEvent::OpenExtensions => { + this.append_log("[Extensions] Opening extensions marketplace...", cx); + cx.notify(); } - ToolbarEvent::ToggleSidebar => { - this.sidebar_visible = !this.sidebar_visible; - let tab = if this.sidebar_visible { - Some(ActivityTab::Explorer) - } else { - None - }; - let _ = this.activity_rail.update(cx, |r, cx| r.set_active_tab(tab, cx)); + PluginRailEvent::ToggleNotifications => { + this.append_log("[Notifications] Toggling notifications tool window...", cx); cx.notify(); } - ToolbarEvent::ToggleTerminal => { - let _ = this.bottom_panel.update(cx, |bp, cx| { - bp.toggle_collapsed(cx); - }); + PluginRailEvent::ToggleMaven => { + let _ = this + .bottom_panel + .update(cx, |bp, cx| bp.set_tab(BottomTab::Output, cx)); + this.append_log("[Maven] Opening Maven tool window...", cx); + cx.notify(); } - ToolbarEvent::ClearTerminal => { - let _ = this.bottom_panel.update(cx, |bp, cx| { - let _ = bp.terminal.update(cx, |term, cx| { - term.clear(cx); + }, + ); + + // 5. 订阅顶部工具栏事件 + let sub_toolbar = + cx.subscribe( + &toolbar, + |this, _toolbar, event: &ToolbarEvent, cx| match event { + ToolbarEvent::NewFile => { + let _ = this.editor.update(cx, |ed, cx| { + ed.open_file("untitled.txt".to_string(), String::new(), cx); }); - }); - } - ToolbarEvent::RefreshWorkspace => { - let _ = this.sidebar.update(cx, |sb, cx| { - sb.refresh(cx); - sb.refresh_git(cx); - }); - } - ToolbarEvent::QuickOpen => { - this.open_search_everywhere(cx); - } - ToolbarEvent::Run => { - let _ = this.bottom_panel.update(cx, |bp, cx| { - bp.append_log("[Run] Executing default run configuration...".to_string(), cx); - let _ = bp.terminal.update(cx, |term, cx| { - term.send_command("echo '[Lithe Run]' && cargo check", cx); + } + ToolbarEvent::Save => { + let _ = this.editor.update(cx, |ed, cx| { + ed.save_active(cx); }); - }); - } - ToolbarEvent::Debug => { - let _ = this.bottom_panel.update(cx, |bp, cx| { - bp.append_log("[Debug] Launching DAP debug session...".to_string(), cx); - }); - } - ToolbarEvent::Stop => { - let _ = this.bottom_panel.update(cx, |bp, cx| { - bp.append_log("[Stop] Session terminated by user.".to_string(), cx); - }); - } - ToolbarEvent::OpenSettings => { - this.show_settings_dialog = true; - cx.notify(); - } - ToolbarEvent::About => { - let _ = this.bottom_panel.update(cx, |bp, cx| { - bp.append_log("[About] Lithe IDE for Linux (Powered by GPUI Kit & Rust Core)".to_string(), cx); - }); - } - ToolbarEvent::Exit => { - cx.quit(); - } - } - }); + } + ToolbarEvent::CloseTab => { + let _ = this.editor.update(cx, |ed, cx| { + if let Some(idx) = ed.active_tab_index { + ed.close_tab(idx, cx); + } + }); + } + ToolbarEvent::ToggleSidebar => { + this.toggle_sidebar(cx); + } + ToolbarEvent::ToggleTerminal => { + let _ = this.bottom_panel.update(cx, |bp, cx| { + bp.toggle_collapsed(cx); + }); + } + ToolbarEvent::ClearTerminal => { + let _ = this.bottom_panel.update(cx, |bp, cx| { + let _ = bp.terminal.update(cx, |term, cx| { + term.clear(cx); + }); + }); + } + ToolbarEvent::RefreshWorkspace => { + let _ = this.sidebar.update(cx, |sb, cx| { + sb.refresh(cx); + sb.refresh_git(cx); + }); + } + ToolbarEvent::QuickOpen => { + this.open_quick_open(cx); + } + ToolbarEvent::Run => { + this.append_log("[Run] Executing default run configuration...", cx); + let _ = this.bottom_panel.update(cx, |bp, cx| { + let _ = bp.terminal.update(cx, |term, cx| { + term.send_command("echo '[Lithe Run]' && cargo check", cx); + }); + }); + } + ToolbarEvent::Debug => { + this.append_log("[Debug] Launching DAP debug session...", cx); + } + ToolbarEvent::Stop => { + this.append_log("[Stop] Session terminated by user.", cx); + } + ToolbarEvent::OpenSettings => { + this.open_settings(cx); + } + ToolbarEvent::About => { + this.append_log( + "[About] Lithe IDE for Linux (Powered by GPUI Kit & Rust Core)", + cx, + ); + } + ToolbarEvent::Exit | ToolbarEvent::WindowClose => { + cx.quit(); + } + ToolbarEvent::ToggleTheme => { + this.toggle_theme(cx); + } + ToolbarEvent::WindowMinimize => { + cx.defer(|cx| { + if let Some(handle) = cx.active_window() { + let _ = handle.update(cx, |_, window, _| window.minimize_window()); + } + }); + } + ToolbarEvent::WindowMaximize => { + cx.defer(|cx| { + if let Some(handle) = cx.active_window() { + let _ = handle.update(cx, |_, window, _| window.zoom_window()); + } + }); + } + ToolbarEvent::MenuAction(id) => { + this.handle_action(id, cx); + } + ToolbarEvent::NewProject + | ToolbarEvent::OpenProject + | ToolbarEvent::CloneRepository => { + this.append_log( + "[Project] Project picker is not wired to the backend yet.", + cx, + ); + } + ToolbarEvent::OpenRecent => { + this.show_welcome = true; + cx.notify(); + } + }, + ); - // 5. 订阅全局搜索事件 (SearchEverywhereEvent) + // 6. 订阅全局搜索事件 let status_bar_search = status_bar.clone(); - let sub_search = cx.subscribe(&search_everywhere, move |this, _search, event: &SearchEverywhereEvent, cx| { - match event { + let sub_search = cx.subscribe( + &search_everywhere, + move |this, _search, event: &SearchEverywhereEvent, cx| match event { SearchEverywhereEvent::OpenFile(path) => { this.open_file(path, cx); let _ = status_bar_search.update(cx, |sb, cx| { @@ -309,50 +398,260 @@ impl WorkbenchView { this.show_search_everywhere = false; cx.notify(); } - } - }); + }, + ); - // 6. 订阅设置对话框事件 (SettingsEvent) - let sub_settings = cx.subscribe(&settings_dialog, |this, _dialog, event: &SettingsEvent, cx| { - match event { + // 7. 订阅快速打开事件 + let status_bar_quick = status_bar.clone(); + let sub_quick_open = cx.subscribe( + &quick_open, + move |this, _qo, event: &QuickOpenEvent, cx| match event { + QuickOpenEvent::OpenFile(path) => { + this.open_file(path, cx); + let _ = status_bar_quick.update(cx, |sb, cx| { + sb.set_file_info( + Some(path.clone()), + 1, + 1, + EditorView::language_name(path).to_string(), + cx, + ); + }); + this.show_quick_open = false; + cx.notify(); + } + QuickOpenEvent::Close => { + this.show_quick_open = false; + cx.notify(); + } + }, + ); + + // 8. 订阅命令面板事件 + let sub_command_palette = cx.subscribe( + &command_palette, + |this, _cp, event: &CommandPaletteEvent, cx| match event { + CommandPaletteEvent::Execute(id) => { + this.handle_action(id, cx); + this.show_command_palette = false; + cx.notify(); + } + CommandPaletteEvent::Close => { + this.show_command_palette = false; + cx.notify(); + } + }, + ); + + // 9. 订阅设置对话框事件 + let sub_settings = cx.subscribe( + &settings_dialog, + |this, _dialog, event: &SettingsEvent, cx| match event { SettingsEvent::Close => { this.show_settings_dialog = false; cx.notify(); } + SettingsEvent::Changed => { + cx.notify(); + } + }, + ); + + // 10. 订阅欢迎页事件 + let sub_welcome = cx.subscribe( + &welcome_screen, + |this, _welcome, event: &WelcomeEvent, cx| match event { + WelcomeEvent::OpenFolder | WelcomeEvent::NewProject => { + this.append_log( + "[Welcome] Project picker is not wired to the backend yet.", + cx, + ); + } + WelcomeEvent::CloneRepository => { + this.append_log( + "[Welcome] Clone repository is not wired to the backend yet.", + cx, + ); + } + WelcomeEvent::OpenProject(path) => { + this.workspace_root = path.clone(); + this.show_welcome = false; + let _ = this.sidebar.update(cx, |sb, cx| { + sb.root_path = path.clone(); + sb.refresh(cx); + sb.refresh_git(cx); + }); + cx.notify(); + } + WelcomeEvent::OpenSettings => { + this.open_settings(cx); + } + WelcomeEvent::RemoveRecent(path) => { + let _ = path; + cx.notify(); + } + }, + ); + + // 11. 订阅状态栏事件 + let sidebar_status = sidebar.clone(); + let sub_status = + cx.subscribe( + &status_bar, + move |this, _sb, event: &StatusBarEvent, cx| match event { + StatusBarEvent::OpenGit => { + this.sidebar_visible = true; + let _ = this + .activity_rail + .update(cx, |r, cx| r.set_active_view(Some("git".to_string()), cx)); + let _ = sidebar_status.update(cx, |sb, cx| sb.set_tab(SidebarTab::Git, cx)); + cx.notify(); + } + }, + ); + + // 12. 窗口外观变化:跟随系统主题时实时切换调色板 + let sub_appearance = cx.observe_window_appearance(window, |this, window, cx| { + let sync = settings::get(cx).sync_system_theme; + if sync { + let is_dark = !matches!( + window.appearance(), + gpui_kit::WindowAppearance::Light | gpui_kit::WindowAppearance::VibrantLight + ); + this.apply_resolved_theme(is_dark, cx); } }); - Self { + let mut view = Self { workspace_root, sidebar_visible: true, - sidebar_width: 260.0, + sidebar_width: settings::get(cx).sidebar_width, bottom_panel_height: 240.0, is_resizing_sidebar: false, is_resizing_bottom_panel: false, drag_start_x: 0.0, drag_start_y: 0.0, initial_resize_size: 0.0, + show_welcome: false, show_search_everywhere: false, + show_quick_open: false, + show_command_palette: false, show_settings_dialog: false, toolbar, activity_rail, + plugin_rail, sidebar, editor, bottom_panel, status_bar, search_everywhere, + quick_open, + command_palette, settings_dialog, + welcome_screen, focus_handle, client: CoreClient::new(), _subscriptions: vec![ sub_sidebar, obs_sidebar, sub_rail, + sub_plugin_rail, sub_toolbar, sub_search, + sub_quick_open, + sub_command_palette, sub_settings, + sub_welcome, + sub_status, + sub_appearance, ], + }; + + // 应用初始主题与状态栏缩进 + let is_dark = { + let s = settings::get(cx); + !crate::theme::ThemePalette::is_light(&settings::resolved_theme_id(s, false)) + }; + view.apply_resolved_theme(is_dark, cx); + let tab_size = settings::get(cx).tab_size; + let _ = view.status_bar.update(cx, |sb, cx| { + sb.indent_size = tab_size; + cx.notify(); + }); + + view + } + + /// 切换侧边栏展开状态;展开时回到活动栏当前视图。 + fn toggle_sidebar(&mut self, cx: &mut Context) { + self.sidebar_visible = !self.sidebar_visible; + let active_view = self.activity_rail.read(cx).active_view.clone(); + if !self.sidebar_visible { + let _ = self + .activity_rail + .update(cx, |r, cx| r.set_active_view(None, cx)); + } else if active_view.is_none() { + let _ = self + .activity_rail + .update(cx, |r, cx| r.set_active_view(Some("files".to_string()), cx)); } + cx.notify(); + } + + /// 切换浅色/深色主题并落盘。 + fn toggle_theme(&mut self, cx: &mut Context) { + let next = { + let s = settings::get(cx); + if s.sync_system_theme { + // 跟随系统时,手动切换视为退出跟随并固定为相反外观。 + let current = settings::resolved_theme_id(s, false); + if crate::theme::ThemePalette::is_light(¤t) { + s.auto_theme_dark.clone() + } else { + s.auto_theme_light.clone() + } + } else if crate::theme::ThemePalette::is_light(&s.theme) { + "lithe-dark".to_string() + } else { + "lithe-light".to_string() + } + }; + let next_for_update = next.clone(); + settings::update(cx, |s| { + s.theme = next_for_update; + s.sync_system_theme = false; + }); + self.apply_resolved_theme(!crate::theme::ThemePalette::is_light(&next), cx); + cx.notify(); + } + + /// 按解析后的外观应用工作台调色板,并同步 gpui-component 主题模式。 + fn apply_resolved_theme(&mut self, system_is_dark: bool, cx: &mut Context) { + let theme_id = { + let s = settings::get(cx); + settings::resolved_theme_id(s, system_is_dark) + }; + settings::apply_theme(&theme_id); + let mode = if crate::theme::ThemePalette::is_light(&theme_id) { + gpui_kit::component::ThemeMode::Light + } else { + gpui_kit::component::ThemeMode::Dark + }; + gpui_kit::component::Theme::change(mode, None, cx); + cx.notify(); + } + + /// 打开设置对话框并按 `lastSettingsTab` 重置分类。 + fn open_settings(&mut self, cx: &mut Context) { + self.show_settings_dialog = true; + let _ = self.settings_dialog.update(cx, |d, cx| d.open(cx)); + cx.notify(); + } + + fn append_log(&mut self, message: &str, cx: &mut Context) { + let _ = self.bottom_panel.update(cx, |bp, cx| { + bp.append_log(message.to_string(), cx); + }); } /// 打开全局搜索弹窗并更新当前文件索引 @@ -365,6 +664,25 @@ impl WorkbenchView { cx.notify(); } + /// 打开快速打开弹窗 + pub fn open_quick_open(&mut self, cx: &mut Context) { + this_sync_files(self, cx); + self.show_quick_open = true; + let _ = self.quick_open.update(cx, |qo, cx| { + qo.reset(cx); + }); + cx.notify(); + } + + /// 打开命令面板 + pub fn open_command_palette(&mut self, cx: &mut Context) { + self.show_command_palette = true; + let _ = self.command_palette.update(cx, |cp, cx| { + cp.reset(cx); + }); + cx.notify(); + } + fn collect_all_file_paths(entry: &FileEntry, output: &mut Vec) { if !entry.is_directory { output.push(entry.path.clone()); @@ -376,44 +694,36 @@ impl WorkbenchView { } } - /// 执行常用命令动作 + /// 执行常用命令动作(工具栏菜单、命令面板、全局搜索共用)。 pub fn handle_action(&mut self, action_id: &str, cx: &mut Context) { match action_id { - "workbench.new_file" => { + "workbench.new_file" | "file.new_file" => { let _ = self.editor.update(cx, |ed, cx| { ed.open_file("untitled.txt".to_string(), String::new(), cx); }); } - "workbench.save" => { + "workbench.save" | "file.save" => { let _ = self.editor.update(cx, |ed, cx| { ed.save_active(cx); }); } - "workbench.close_tab" => { + "workbench.close_tab" | "file.close_editor" => { let _ = self.editor.update(cx, |ed, cx| { if let Some(idx) = ed.active_tab_index { ed.close_tab(idx, cx); } }); } - "workbench.toggle_terminal" => { + "workbench.toggle_terminal" | "view.toggle_bottom_panel" | "terminal.new" => { let _ = self.bottom_panel.update(cx, |bp, cx| { bp.toggle_collapsed(cx); }); } - "workbench.toggle_sidebar" => { - self.sidebar_visible = !self.sidebar_visible; - let tab = if self.sidebar_visible { - Some(ActivityTab::Explorer) - } else { - None - }; - let _ = self.activity_rail.update(cx, |r, cx| r.set_active_tab(tab, cx)); - cx.notify(); + "workbench.toggle_sidebar" | "view.toggle_sidebar" => { + self.toggle_sidebar(cx); } - "workbench.open_settings" => { - self.show_settings_dialog = true; - cx.notify(); + "workbench.open_settings" | "tools.settings" => { + self.open_settings(cx); } "workbench.refresh_workspace" => { let _ = self.sidebar.update(cx, |sb, cx| { @@ -421,26 +731,62 @@ impl WorkbenchView { sb.refresh_git(cx); }); } - "workbench.run" => { + "workbench.run" | "run.run" => { + self.append_log("[Run] Executing default run configuration...", cx); let _ = self.bottom_panel.update(cx, |bp, cx| { - bp.append_log("[Run] Executing default run configuration...".to_string(), cx); let _ = bp.terminal.update(cx, |term, cx| { term.send_command("cargo check", cx); }); }); } - "workbench.debug" => { - let _ = self.bottom_panel.update(cx, |bp, cx| { - bp.append_log("[Debug] Launching DAP debug session...".to_string(), cx); - }); + "workbench.debug" | "run.debug" => { + self.append_log("[Debug] Launching DAP debug session...", cx); } - "workbench.clear_terminal" => { + "workbench.clear_terminal" | "terminal.clear" => { let _ = self.bottom_panel.update(cx, |bp, cx| { let _ = bp.terminal.update(cx, |term, cx| { term.clear(cx); }); }); } + "view.quick_open" | "go.go_to_file" => { + self.open_quick_open(cx); + } + "view.command_palette" => { + self.open_command_palette(cx); + } + "view.toggle_status_bar" => { + let current = settings::get(cx).show_status_bar; + settings::update(cx, |s| s.show_status_bar = !current); + cx.notify(); + } + "view.welcome" => { + self.show_welcome = !self.show_welcome; + cx.notify(); + } + "help.about" => { + self.append_log( + "[About] Lithe IDE for Linux (Powered by GPUI Kit & Rust Core)", + cx, + ); + } + "file.exit" | "window.close" => { + cx.quit(); + } + "window.minimize" => { + cx.defer(|cx| { + if let Some(handle) = cx.active_window() { + let _ = handle.update(cx, |_, window, _| window.minimize_window()); + } + }); + } + "window.maximize" => { + cx.defer(|cx| { + if let Some(handle) = cx.active_window() { + let _ = handle.update(cx, |_, window, _| window.zoom_window()); + } + }); + } _ => {} } } @@ -473,6 +819,25 @@ impl WorkbenchView { } } +/// 活动栏视图 id → 侧边栏标签。 +fn sidebar_tab_for(view_id: &str) -> Option { + match view_id { + "files" => Some(SidebarTab::Explorer), + "search" => Some(SidebarTab::Search), + "git" => Some(SidebarTab::Git), + _ => None, + } +} + +/// 活动栏底部项 id → 底部面板标签。 +fn bottom_tab_for(pane_id: &str) -> Option { + match pane_id { + "terminal" | "run" | "maven" | "gitLog" => Some(BottomTab::Output), + "diagnostics" => Some(BottomTab::Problems), + _ => None, + } +} + fn this_sync_files(this: &WorkbenchView, cx: &mut Context) { let mut file_list = Vec::new(); if let Some(root_node) = &this.sidebar.read(cx).root_node { @@ -480,55 +845,67 @@ fn this_sync_files(this: &WorkbenchView, cx: &mut Context) { } if !file_list.is_empty() { let _ = this.search_everywhere.update(cx, |search, cx| { - search.set_files(file_list, cx); + search.set_files(file_list.clone(), cx); + }); + let _ = this.quick_open.update(cx, |qo, cx| { + qo.set_files(file_list, cx); }); } } impl Render for WorkbenchView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let show_status_bar = settings::get(cx).show_status_bar; + div() .track_focus(&self.focus_handle) .relative() .size_full() - .bg(ThemeColors::bg_editor()) + .bg(ThemeColors::background()) .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { let key = event.keystroke.key.as_str(); - // 键盘快捷键监听 - if event.keystroke.modifiers.control || event.keystroke.modifiers.platform { + let modifiers = event.keystroke.modifiers; + + // 浮层优先:Esc 关闭最上层模态。 + if key == "escape" { + if this.show_command_palette { + this.show_command_palette = false; + } else if this.show_quick_open { + this.show_quick_open = false; + } else if this.show_search_everywhere { + this.show_search_everywhere = false; + } else if this.show_settings_dialog { + this.show_settings_dialog = false; + } + cx.notify(); + return; + } + + if modifiers.control || modifiers.platform { + if modifiers.shift && key == "p" { + this.open_command_palette(cx); + return; + } + if modifiers.shift && key == "f" { + this.open_search_everywhere(cx); + return; + } match key { - "p" => { - this.open_search_everywhere(cx); - } - "," => { - this.show_settings_dialog = true; - cx.notify(); - } - "b" => { - this.sidebar_visible = !this.sidebar_visible; - let tab = if this.sidebar_visible { - Some(ActivityTab::Explorer) - } else { - None - }; - let _ = this.activity_rail.update(cx, |r, cx| r.set_active_tab(tab, cx)); - cx.notify(); - } + "p" => this.open_quick_open(cx), + "," => this.open_settings(cx), + "b" => this.toggle_sidebar(cx), "`" => { let _ = this.bottom_panel.update(cx, |bp, cx| { bp.toggle_collapsed(cx); }); } + "j" => { + let _ = this.bottom_panel.update(cx, |bp, cx| { + bp.set_tab(BottomTab::Terminal, cx); + }); + } _ => {} } - } else if key == "escape" { - if this.show_search_everywhere { - this.show_search_everywhere = false; - cx.notify(); - } else if this.show_settings_dialog { - this.show_settings_dialog = false; - cx.notify(); - } } })) .on_mouse_move(cx.listener(|this, event: &MouseMoveEvent, _window, cx| { @@ -556,110 +933,124 @@ impl Render for WorkbenchView { .on_mouse_up( MouseButton::Left, cx.listener(|this, _event: &MouseUpEvent, _window, cx| { - if this.is_resizing_sidebar || this.is_resizing_bottom_panel { + if this.is_resizing_sidebar { this.is_resizing_sidebar = false; + let width = this.sidebar_width; + settings::update(cx, |s| s.sidebar_width = width); + cx.notify(); + } + if this.is_resizing_bottom_panel { this.is_resizing_bottom_panel = false; cx.notify(); } }), ) + .child(if self.show_welcome { + self.welcome_screen.clone().into_any_element() + } else { + self.render_workbench(show_status_bar, cx) + .into_any_element() + }) + .when(self.show_search_everywhere, |view| { + view.child(self.search_everywhere.clone()) + }) + .when(self.show_quick_open, |view| { + view.child(self.quick_open.clone()) + }) + .when(self.show_command_palette, |view| { + view.child(self.command_palette.clone()) + }) + .when(self.show_settings_dialog, |view| { + view.child(self.settings_dialog.clone()) + }) + } +} + +impl WorkbenchView { + /// 渲染工作台主体(顶栏 / 侧边栏 / 编辑区 / 底部面板 / 状态栏)。 + fn render_workbench(&self, show_status_bar: bool, cx: &mut Context) -> impl IntoElement { + v_flex() + .size_full() + .bg(ThemeColors::background()) + .child(self.toolbar.clone()) .child( - v_flex() - .size_full() - .bg(ThemeColors::bg_editor()) - .child( - // 1. 顶部标题栏/工具栏 (Toolbar) - self.toolbar.clone(), - ) - .child( - // 2. 主工作区区域(ActivityRail + 侧边栏 + 可调节垂直Splitter + 编辑器主岛) - h_flex() - .flex_1() - .w_full() - .child( - // 左侧垂直图标活动栏 (Activity Rail) - self.activity_rail.clone(), - ) - .when(self.sidebar_visible, |layout| { - layout - .child( - // 侧边栏内容面板 (Explorer / Search / Git) - div() - .w(px(self.sidebar_width)) - .h_full() - .flex_shrink_0() - .child(self.sidebar.clone()), - ) - .child( - // 侧边栏与主编辑器之间的精致 Splitter 分隔条 - div() - .id("sidebar-splitter") - .w(px(4.0)) - .h_full() - .cursor_col_resize() - .bg(if self.is_resizing_sidebar { - ThemeColors::accent_blue() - } else { - ThemeColors::border() - }) - .hover(|h| h.bg(ThemeColors::accent_blue())) - .on_mouse_down( - MouseButton::Left, - cx.listener(|this, event: &MouseDownEvent, _window, cx| { - this.is_resizing_sidebar = true; - this.drag_start_x = f32::from(event.position.x); - this.initial_resize_size = this.sidebar_width; - cx.notify(); - }), - ), - ) - }) + // 主工作区:左活动栏 + 侧边栏 + 编辑区 + 右插件活动栏 + h_flex() + .flex_1() + .w_full() + .min_h_0() + .child(self.activity_rail.clone()) + .when(self.sidebar_visible, |layout| { + layout .child( - // 中央编辑器主岛 div() - .flex_1() + .w(px(self.sidebar_width)) .h_full() - .bg(ThemeColors::bg_editor()) - .child(self.editor.clone()), - ), - ) + .flex_shrink_0() + .child(self.sidebar.clone()), + ) + .child(self.render_sidebar_splitter(cx)) + }) .child( - // 主编辑器与底栏之间的精致 Splitter 分隔条 div() - .id("bottom-panel-splitter") - .h(px(4.0)) - .w_full() - .cursor_row_resize() - .bg(if self.is_resizing_bottom_panel { - ThemeColors::accent_blue() - } else { - ThemeColors::border() - }) - .hover(|h| h.bg(ThemeColors::accent_blue())) - .on_mouse_down( - MouseButton::Left, - cx.listener(|this, event: &MouseDownEvent, _window, cx| { - this.is_resizing_bottom_panel = true; - this.drag_start_y = f32::from(event.position.y); - this.initial_resize_size = this.bottom_panel_height; - cx.notify(); - }), - ), - ) - .child( - // 3. 底部抽屉面板 (Terminal / Output / Problems) - self.bottom_panel.clone(), + .flex_1() + .h_full() + .min_w_0() + .bg(ThemeColors::background()) + .child(self.editor.clone()), ) - .child( - // 4. 最底部状态栏 (Status Bar) - self.status_bar.clone(), - ), + .child(self.plugin_rail.clone()), ) - .when(self.show_search_everywhere, |view| { - view.child(self.search_everywhere.clone()) + .child(self.render_bottom_splitter(cx)) + .child(self.bottom_panel.clone()) + .when(show_status_bar, |layout| { + layout.child(self.status_bar.clone()) }) - .when(self.show_settings_dialog, |view| { - view.child(self.settings_dialog.clone()) + } + + fn render_sidebar_splitter(&self, cx: &mut Context) -> impl IntoElement { + div() + .id("sidebar-splitter") + .w(px(4.0)) + .h_full() + .cursor_col_resize() + .bg(if self.is_resizing_sidebar { + ThemeColors::primary() + } else { + ThemeColors::border() }) + .hover(|h| h.bg(ThemeColors::primary())) + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, event: &MouseDownEvent, _window, cx| { + this.is_resizing_sidebar = true; + this.drag_start_x = f32::from(event.position.x); + this.initial_resize_size = this.sidebar_width; + cx.notify(); + }), + ) + } + + fn render_bottom_splitter(&self, cx: &mut Context) -> impl IntoElement { + div() + .id("bottom-panel-splitter") + .h(px(4.0)) + .w_full() + .cursor_row_resize() + .bg(if self.is_resizing_bottom_panel { + ThemeColors::primary() + } else { + ThemeColors::border() + }) + .hover(|h| h.bg(ThemeColors::primary())) + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, event: &MouseDownEvent, _window, cx| { + this.is_resizing_bottom_panel = true; + this.drag_start_y = f32::from(event.position.y); + this.initial_resize_size = this.bottom_panel_height; + cx.notify(); + }), + ) } } diff --git a/linux/src/workbench/welcome_screen.rs b/linux/src/workbench/welcome_screen.rs new file mode 100644 index 000000000..1c886080d --- /dev/null +++ b/linux/src/workbench/welcome_screen.rs @@ -0,0 +1,544 @@ +//! 工作台欢迎页(Welcome Screen)。 +//! +//! 复刻 Tauri 端 `features/layout/components/welcome-screen.tsx`:左侧 240px 导航栏 +//! (Lithe 标识、Projects 高亮项、底部 Settings),右侧标题、搜索输入框与 +//! Clone/Open 操作按钮、最近项目列表。所有交互只向外广播 [`WelcomeEvent`], +//! 不接入真实的项目打开、克隆与设置流程。 + +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, AnyElement, Context, EventEmitter, FocusHandle, FontWeight, InteractiveElement as _, + IntoElement, KeyDownEvent, ParentElement as _, Render, StatefulInteractiveElement as _, + Styled as _, Window, +}; + +use crate::theme::ThemeColors; + +/// 最近项目条目。 +/// +/// `last_opened` 为展示用的相对时间文案,由上层决定格式,本模块不解析时间。 +#[derive(Debug, Clone)] +pub struct RecentProject { + pub name: String, + pub path: String, + pub last_opened: String, +} + +/// 欢迎页对外事件。 +#[derive(Debug, Clone)] +pub enum WelcomeEvent { + /// 打开文件夹(Open 按钮)。 + OpenFolder, + /// 新建项目。 + #[allow(dead_code)] + NewProject, + /// 从远程克隆仓库(Clone 按钮)。 + CloneRepository, + /// 打开指定路径的最近项目,携带项目路径。 + OpenProject(String), + /// 打开设置。 + OpenSettings, + /// 从最近列表移除某项目,携带项目路径。 + RemoveRecent(String), +} + +#[allow(dead_code)] +impl WelcomeEvent { + /// 供视图层在事件被消费前读取载荷,避免“字段未读”噪声。 + pub fn payload(&self) -> Option<&str> { + match self { + WelcomeEvent::OpenProject(path) | WelcomeEvent::RemoveRecent(path) => Some(path), + _ => None, + } + } +} + +/// 整屏欢迎页:左侧导航栏 + 右侧项目区。 +pub struct WelcomeScreenView { + pub recent_projects: Vec, + pub selected_index: usize, + pub query: String, + pub focus_handle: FocusHandle, +} + +impl EventEmitter for WelcomeScreenView {} + +impl WelcomeScreenView { + pub fn new(cx: &mut Context) -> Self { + Self { + recent_projects: default_recent_projects(), + selected_index: 0, + query: String::new(), + focus_handle: cx.focus_handle(), + } + } + + /// 当前查询过滤后的最近项目。 + fn visible_projects(&self) -> Vec<&RecentProject> { + let q = self.query.trim().to_lowercase(); + self.recent_projects + .iter() + .filter(|p| { + q.is_empty() + || p.name.to_lowercase().contains(&q) + || p.path.to_lowercase().contains(&q) + }) + .collect() + } + + /// 当前有效选中下标(对空列表安全)。 + fn current_index(&self) -> usize { + let total = self.visible_projects().len(); + if total == 0 { + 0 + } else { + self.selected_index.min(total - 1) + } + } + + /// 打开可见列表中第 `position` 个项目。 + fn open_at(&self, position: usize, cx: &mut Context) { + let visible = self.visible_projects(); + if let Some(project) = visible.get(position) { + cx.emit(WelcomeEvent::OpenProject(project.path.clone())); + } + } + + /// 从最近列表移除指定路径,并夹紧选中下标。 + fn remove_recent(&mut self, path: &str, cx: &mut Context) { + self.recent_projects.retain(|p| p.path != path); + let total = self.visible_projects().len(); + if self.selected_index >= total { + self.selected_index = total.saturating_sub(1); + } + cx.emit(WelcomeEvent::RemoveRecent(path.to_string())); + cx.notify(); + } + + /// 左侧导航栏:Lithe 标识、Projects 高亮项、底部 Settings。 + fn render_sidebar(&self, cx: &mut Context) -> AnyElement { + v_flex() + .w(px(240.0)) + .h_full() + .flex_shrink_0() + .bg(ThemeColors::surface()) + .border_r_1() + .border_color(ThemeColors::border()) + .child( + // 顶部标识区:Logo + 名称 + 版本 + h_flex() + .items_center() + .gap_2p5() + .px_4() + .pt_6() + .pb_4() + .child( + Icon::new(IconName::Zap) + .size(px(24.0)) + .text_color(ThemeColors::primary()), + ) + .child( + v_flex() + .child( + div() + .text_lg() + .font_weight(FontWeight::BOLD) + .text_color(ThemeColors::foreground()) + .child("Lithe"), + ) + .child( + div() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child("0.1.0 · Linux"), + ), + ), + ) + .child( + // Projects 高亮项 + h_flex() + .id("welcome-projects") + .mx_2() + .h(px(36.0)) + .items_center() + .gap_2p5() + .px_3() + .rounded_md() + .bg(ThemeColors::selected()) + .cursor_pointer() + .child( + Icon::new(IconName::Folder) + .size(px(15.0)) + .text_color(ThemeColors::primary()), + ) + .child( + div() + .text_sm() + .font_weight(FontWeight::MEDIUM) + .text_color(ThemeColors::foreground()) + .child("Projects"), + ), + ) + .child(div().flex_1()) + .child( + // 底部 Settings 项 + h_flex() + .id("welcome-settings") + .mx_2() + .mb_2() + .h(px(36.0)) + .items_center() + .gap_2p5() + .px_3() + .rounded_md() + .cursor_pointer() + .text_color(ThemeColors::subtle_foreground()) + .hover(|h| h.bg(ThemeColors::accent())) + .child( + Icon::new(IconName::Settings) + .size(px(15.0)) + .text_color(ThemeColors::subtle_foreground()), + ) + .child(div().text_sm().child("Settings")) + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(WelcomeEvent::OpenSettings); + })), + ) + .into_any_element() + } + + /// 右侧项目区:标题、搜索与操作按钮、最近项目列表。 + fn render_main(&self, cx: &mut Context) -> AnyElement { + let visible = self.visible_projects(); + let current_index = self.current_index(); + + v_flex() + .flex_1() + .h_full() + .min_w_0() + .px_5() + .pt_10() + .child( + // 大标题 + h_flex().w_full().justify_center().child( + div() + .text_xl() + .font_weight(FontWeight::BOLD) + .text_color(ThemeColors::foreground()) + .child("Welcome to Lithe"), + ), + ) + .child( + h_flex().w_full().justify_center().pt_2().child( + div() + .text_sm() + .text_color(ThemeColors::subtle_foreground()) + .child("Open a project to get started"), + ), + ) + .child( + // 搜索输入框 + Clone / Open 按钮 + h_flex() + .w_full() + .items_center() + .gap_3() + .mt_8() + .pb_3() + .border_b_1() + .border_color(ThemeColors::border()) + .child( + h_flex() + .w(px(360.0)) + .h(px(36.0)) + .items_center() + .gap_2() + .px_2p5() + .rounded_md() + .bg(ThemeColors::background()) + .border_1() + .border_color(ThemeColors::border()) + .child( + Icon::new(IconName::Search) + .size(px(14.0)) + .text_color(ThemeColors::subtle_foreground()), + ) + .child( + div() + .flex_1() + .text_sm() + .text_color(if self.query.is_empty() { + ThemeColors::subtle_foreground() + } else { + ThemeColors::foreground() + }) + .child(if self.query.is_empty() { + "Search projects".to_string() + } else { + self.query.clone() + }), + ), + ) + .child(div().flex_1()) + .child( + Button::new("welcome-clone") + .small() + .icon(IconName::GitBranch) + .label("Clone") + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(WelcomeEvent::CloneRepository); + })), + ) + .child( + Button::new("welcome-open") + .small() + .primary() + .icon(IconName::FolderOpen) + .label("Open") + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(WelcomeEvent::OpenFolder); + })), + ), + ) + .child( + // 最近项目列表或空态 + div() + .flex_1() + .w_full() + .min_w_0() + .overflow_y_scrollbar() + .py_2() + .when(visible.is_empty(), |list| { + list.child( + v_flex() + .size_full() + .items_center() + .justify_center() + .gap_2() + .child( + Icon::new(IconName::FolderOpen) + .size(px(28.0)) + .text_color(ThemeColors::subtle_foreground()), + ) + .child( + div() + .text_sm() + .font_weight(FontWeight::MEDIUM) + .text_color(ThemeColors::foreground()) + .child("No recent projects"), + ) + .child( + div() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child("Open a folder to start working"), + ), + ) + }) + .children(visible.into_iter().enumerate().map(|(position, project)| { + let is_selected = position == current_index; + let name = project.name.clone(); + let path = project.path.clone(); + let last_opened = project.last_opened.clone(); + let remove_path = project.path.clone(); + let badge = project_badge(&name, &path); + // 每行使用独立的 hover group,避免兄弟行共享 group 名互相影响 + let group_name = format!("welcome-recent-group-{position}"); + + h_flex() + .id(("welcome-recent", position)) + .group(group_name.clone()) + .h(px(52.0)) + .w_full() + .min_w_0() + .items_center() + .gap_3() + .px_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(badge) + .child( + v_flex() + .flex_1() + .min_w_0() + .child( + div() + .text_sm() + .font_weight(FontWeight::MEDIUM) + .text_color(ThemeColors::foreground()) + .child(name), + ) + .child( + h_flex() + .gap_2() + .child( + div() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child(path), + ) + .child( + div() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child(last_opened), + ), + ), + ) + .child( + // 悬停时才显示的删除按钮 + div() + .opacity(0.0) + .group_hover(group_name, |style| style.opacity(1.0)) + .child( + Button::new(("welcome-remove", position)) + .xsmall() + .ghost() + .icon(IconName::Trash) + .tooltip("Remove from recent") + .on_click(cx.listener( + move |this, _event, _window, cx| { + // 阻止冒泡到整行,避免删除时同时打开项目 + cx.stop_propagation(); + this.remove_recent(&remove_path, cx); + }, + )), + ), + ) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.open_at(position, cx); + })) + .into_any_element() + })), + ) + .into_any_element() + } +} + +impl Render for WelcomeScreenView { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + // 请求聚焦以接收键盘上下移动 + window.focus(&self.focus_handle, cx); + + div() + .id("welcome-screen") + .track_focus(&self.focus_handle) + .size_full() + .bg(ThemeColors::background()) + .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { + let key = event.keystroke.key.as_str(); + match key { + "up" | "arrowup" => { + if this.selected_index > 0 { + this.selected_index -= 1; + cx.notify(); + } + } + "down" | "arrowdown" => { + let total = this.visible_projects().len(); + if total > 0 && this.selected_index + 1 < total { + this.selected_index += 1; + cx.notify(); + } + } + "enter" => { + let idx = this.current_index(); + this.open_at(idx, 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(); + } + } + } + } + })) + .child( + h_flex() + .size_full() + .child(self.render_sidebar(cx)) + .child(self.render_main(cx)), + ) + } +} + +/// 默认示例最近项目。 +/// +/// 使用当前工作目录派生的占位路径,避免写入任何真实用户目录。 +fn default_recent_projects() -> Vec { + let base = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); + let sample = |name: &str, last_opened: &str| RecentProject { + name: name.to_string(), + path: base.join(name).to_string_lossy().to_string(), + last_opened: last_opened.to_string(), + }; + + vec![ + sample("lithe-sandbox", "2 hours ago"), + sample("example-project", "yesterday"), + sample("gpui-playground", "last week"), + ] +} + +/// 按名称首字母从语义色板中取色,生成圆角首字母徽标。 +fn project_badge(name: &str, path: &str) -> AnyElement { + let initial = name + .chars() + .find(|c| c.is_alphanumeric()) + .map(|c| c.to_uppercase().to_string()) + .unwrap_or_else(|| "L".to_string()); + + // 用路径字符和做稳定散列,保证同一项目每次渲染颜色一致 + let hash: u32 = path.chars().map(|c| c as u32).sum(); + let color = match hash % 5 { + 0 => ThemeColors::primary(), + 1 => ThemeColors::success(), + 2 => ThemeColors::warning(), + 3 => ThemeColors::destructive(), + _ => ThemeColors::info(), + }; + + h_flex() + .size(px(34.0)) + .flex_shrink_0() + .items_center() + .justify_center() + .rounded_lg() + .bg(color) + .child( + div() + .text_sm() + .font_weight(FontWeight::BOLD) + .text_color(ThemeColors::background()) + .child(initial), + ) + .into_any_element() +} From df39ffab87e32b717d4464867813593db43f89ea Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 12:02:31 +0800 Subject: [PATCH 06/59] =?UTF-8?q?fix(linux):=20=E6=B5=AE=E5=B1=82=E5=8D=A1?= =?UTF-8?q?=E7=89=87=E7=82=B9=E5=87=BB=E4=B8=8D=E5=86=8D=E8=AF=AF=E8=A7=A6?= =?UTF-8?q?=E5=8F=91=E9=81=AE=E7=BD=A9=E5=85=B3=E9=97=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 设置弹窗与全局搜索弹窗的卡片 on_mouse_down 是空处理器,未阻断事件冒泡, 点击卡片内的分类项/控件会冒泡到遮罩并触发 Close。补上 cx.stop_propagation(), 与命令面板、快速打开保持一致。 --- linux/src/workbench/search_everywhere.rs | 5 +++-- linux/src/workbench/settings_dialog.rs | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/linux/src/workbench/search_everywhere.rs b/linux/src/workbench/search_everywhere.rs index 4ce398b58..a8a7062af 100644 --- a/linux/src/workbench/search_everywhere.rs +++ b/linux/src/workbench/search_everywhere.rs @@ -322,8 +322,9 @@ impl Render for SearchEverywhereModal { .overflow_hidden() .on_mouse_down( gpui_kit::MouseButton::Left, - cx.listener(|_this, _event, _window, _cx| { - // 阻止向外层遮罩冒泡关闭 + cx.listener(|_this, _event, _window, cx| { + // 卡片内点击必须阻断冒泡,否则会触发遮罩的关闭逻辑。 + cx.stop_propagation(); }), ) .child( diff --git a/linux/src/workbench/settings_dialog.rs b/linux/src/workbench/settings_dialog.rs index 86b16465f..155297809 100644 --- a/linux/src/workbench/settings_dialog.rs +++ b/linux/src/workbench/settings_dialog.rs @@ -265,8 +265,9 @@ impl Render for SettingsDialog { .overflow_hidden() .on_mouse_down( gpui_kit::MouseButton::Left, - cx.listener(|_this, _event, _window, _cx| { - // 阻止向遮罩冒泡 + cx.listener(|_this, _event, _window, cx| { + // 卡片内点击必须阻断冒泡,否则会触发遮罩的关闭逻辑。 + cx.stop_propagation(); }), ) // 1. 头部:标题、搜索占位、关闭按钮 From aeb1da330a441a7b0f74392756de572702ba240f Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 12:20:46 +0800 Subject: [PATCH 07/59] =?UTF-8?q?fix(linux):=20=E7=B4=A7=E5=87=91=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E6=94=B9=E4=B8=BA=E6=A8=AA=E5=90=91=E6=B5=AE=E5=87=BA?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=E6=9D=A1=EF=BC=8C=E5=AF=B9=E9=BD=90=20Tauri?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原实现是单个 Menu 按钮 + 九个子菜单的竖向下拉,与 Tauri `compactFloating` 的 Menubar 形态不一致。改为点击 Menu 图标后在标题栏下方浮出一个横向圆角 菜单条,九个菜单名并排、各自下拉;选择任意菜单项或点击菜单条外部即收起, 与 Tauri 的 onCompactClose 行为一致。图标由 Menu 换成 List 以对齐 ListIcon。 --- linux/src/workbench/toolbar.rs | 104 +++++++++++++++++++++++++-------- 1 file changed, 80 insertions(+), 24 deletions(-) diff --git a/linux/src/workbench/toolbar.rs b/linux/src/workbench/toolbar.rs index 78fc10da8..1d55bfb65 100644 --- a/linux/src/workbench/toolbar.rs +++ b/linux/src/workbench/toolbar.rs @@ -7,10 +7,12 @@ use gpui_kit::assets::IconName; use gpui_kit::component::button::{Button, ButtonVariants as _}; use gpui_kit::component::menu::{DropdownMenu as _, PopupMenu, PopupMenuItem}; -use gpui_kit::component::{h_flex, Icon, Sizable as _}; +use gpui_kit::component::{h_flex, Icon, Selectable as _, Sizable as _}; +use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::{ - div, px, AnyElement, Context, EventEmitter, FontWeight, InteractiveElement as _, IntoElement, - ParentElement as _, Render, SharedString, StatefulInteractiveElement as _, Styled as _, Window, + div, px, Anchor, AnyElement, Context, EventEmitter, FontWeight, InteractiveElement as _, + IntoElement, ParentElement as _, Render, SharedString, StatefulInteractiveElement as _, + Styled as _, Window, }; use crate::settings; @@ -52,6 +54,8 @@ pub enum ToolbarEvent { pub struct ToolbarView { pub workspace_name: String, pub git_branch: Option, + /// 紧凑菜单条是否展开(对齐 Tauri `isCompactMenuVisible`)。 + compact_menu_open: bool, } impl EventEmitter for ToolbarView {} @@ -66,6 +70,7 @@ impl ToolbarView { Self { workspace_name: name, git_branch: None, + compact_menu_open: false, } } @@ -236,8 +241,10 @@ fn menu_action_item( ) -> PopupMenuItem { let v = view.clone(); PopupMenuItem::new(label).on_click(move |_, _, cx| { - v.update(cx, |_, cx| { - cx.emit(ToolbarEvent::MenuAction(action.to_string())) + v.update(cx, |this, cx| { + // 选中任意菜单项后收起紧凑菜单条,与 Tauri `onCompactClose` 一致。 + this.compact_menu_open = false; + cx.emit(ToolbarEvent::MenuAction(action.to_string())); }); }) } @@ -251,7 +258,10 @@ fn event_item( let v = view.clone(); PopupMenuItem::new(label).on_click(move |_, _, cx| { let event = make(); - v.update(cx, |_, cx| cx.emit(event)); + v.update(cx, |this, cx| { + this.compact_menu_open = false; + cx.emit(event); + }); }) } @@ -297,25 +307,71 @@ impl Render for ToolbarView { let view = cx.entity(); - // 应用菜单栏:紧凑模式为单个 Menu 按钮 + 九个子菜单;否则平铺九个顶层菜单。 + // 应用菜单栏:紧凑模式为 Menu 图标触发一个横向浮出的菜单条(九个菜单名并排, + // 各自下拉),对齐 Tauri `compactFloating` 的 `Menubar`;非紧凑模式平铺九个顶层菜单。 let app_menu: AnyElement = if compact_menu { - let v = view.clone(); - Button::new("tb-app-menu") - .small() - .ghost() - .icon(IconName::Menu) - .tooltip("Menu") - .dropdown_menu(move |menu, window, cx| { - let mut menu = menu; - for app in APP_MENUS { - let v_sub = v.clone(); - menu = menu.submenu(app.title, window, cx, move |sub, _window, _cx| { - build_menu(sub, app.groups, &v_sub) - }); - } - menu - }) - .into_any_element() + let v_menu = view.clone(); + // 触发点相对定位,浮层用 deferred+anchored 挂在其下方,避免撑开标题栏。 + let open = self.compact_menu_open; + let trigger = div() + .id("tb-app-menu") + .relative() + .child( + Button::new("tb-app-menu-btn") + .small() + .ghost() + .icon(IconName::List) + .tooltip("Menu") + .selected(open) + .on_click(cx.listener( + |this, _event: &gpui_kit::ClickEvent, _window, cx| { + this.compact_menu_open = !this.compact_menu_open; + cx.notify(); + }, + )), + ) + .when(open, move |this| { + let v_out = v_menu.clone(); + this.child(gpui_kit::deferred( + gpui_kit::anchored() + .anchor(Anchor::TopLeft) + .snap_to_window_with_margin(px(8.0)) + .child( + h_flex() + .id("tb-compact-menu-bar") + .occlude() + .flex_nowrap() + .items_center() + .gap_0p5() + .px_1() + .py_1() + .top(px(4.0)) + .rounded_xl() + .bg(ThemeColors::background()) + .border_1() + .border_color(ThemeColors::border()) + .shadow_lg() + // 点击菜单条以外区域时收起,对齐 Tauri 紧凑菜单的行为。 + .on_mouse_down_out(move |_, _, cx| { + v_out.update(cx, |this, cx| { + this.compact_menu_open = false; + cx.notify(); + }); + }) + .children(APP_MENUS.iter().map(|app| { + let v_item = v_menu.clone(); + Button::new(format!("tb-compact-menu-{}", app.title)) + .small() + .ghost() + .label(app.title) + .dropdown_menu(move |menu, _window, _cx| { + build_menu(menu, app.groups, &v_item) + }) + })), + ), + )) + }); + trigger.into_any_element() } else { h_flex() .items_center() From 1d50d05a10e416b6552c05417aaf2a17c97d23d3 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 12:27:55 +0800 Subject: [PATCH 08/59] =?UTF-8?q?fix(linux):=20=E5=88=A0=E9=99=A4=20Tauri?= =?UTF-8?q?=20=E4=B8=8D=E5=AD=98=E5=9C=A8=E7=9A=84=E9=A1=B6=E6=A0=8F?= =?UTF-8?q?=E5=85=83=E7=B4=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对照 windows/tauri 的 title-bar.tsx,非 macOS 顶栏只有四组元素: 菜单(紧凑按钮/平铺菜单)、项目菜单(logo+项目名+ChevronDown)、 分支胶囊、右侧搜索图标按钮、窗口控件。 删除 Linux 多出的元素: - ⚡ + "Lithe" 品牌标识 - 中间 280px 全局搜索条(含占位文案与 Ctrl+P 徽标) - "[Project] Default" 运行目标胶囊 - Run / Debug / Stop 按钮 - 主题切换按钮(Sun/Moon) - 设置按钮(Settings) 其余对齐: - 顶栏背景改用 surface(与 Tauri bg-surface 一致)。 - 项目菜单触发器改为 logo 徽标(品牌色圆角方块)+ 项目名 + ChevronDown, 下拉项顺序对齐 TitleProjectMenu:New Project / Open / Clone Repository / Open Projects / Open Recent。 - 右侧只保留搜索图标按钮(quickOpenAction)与窗口控件。 --- linux/src/workbench/toolbar.rs | 266 ++++++++------------------------- 1 file changed, 66 insertions(+), 200 deletions(-) diff --git a/linux/src/workbench/toolbar.rs b/linux/src/workbench/toolbar.rs index 1d55bfb65..ec2e162ae 100644 --- a/linux/src/workbench/toolbar.rs +++ b/linux/src/workbench/toolbar.rs @@ -10,9 +10,8 @@ use gpui_kit::component::menu::{DropdownMenu as _, PopupMenu, PopupMenuItem}; use gpui_kit::component::{h_flex, Icon, Selectable as _, Sizable as _}; use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::{ - div, px, Anchor, AnyElement, Context, EventEmitter, FontWeight, InteractiveElement as _, - IntoElement, ParentElement as _, Render, SharedString, StatefulInteractiveElement as _, - Styled as _, Window, + div, px, Anchor, AnyElement, Context, EventEmitter, InteractiveElement as _, IntoElement, + ParentElement as _, Render, SharedString, StatefulInteractiveElement as _, Styled as _, Window, }; use crate::settings; @@ -293,17 +292,7 @@ impl Render for ToolbarView { let project_name = self.workspace_name.clone(); // 读取渲染所需的设置快照,随后立刻释放对 cx 的只读借用。 - let (compact_menu, theme_icon) = { - let s = settings::get(cx); - let icon = if s.sync_system_theme { - IconName::Moon - } else if s.theme == "lithe-light" { - IconName::Sun - } else { - IconName::Moon - }; - (s.compact_menu_bar, icon) - }; + let compact_menu = settings::get(cx).compact_menu_bar; let view = cx.entity(); @@ -389,63 +378,53 @@ impl Render for ToolbarView { h_flex() .h(px(40.0)) .w_full() - .bg(ThemeColors::background()) + .bg(ThemeColors::surface()) .border_b_1() .border_color(ThemeColors::border()) .items_center() - .pl_3() - // 1. 左侧:应用菜单 + 品牌 + 项目胶囊 + 分支胶囊 + .justify_between() + .px_3() + // 左侧:应用菜单 + 项目菜单 + 分支胶囊(对齐 Tauri `title-bar.tsx` 的左侧组) .child( h_flex() - .flex_1() .min_w_0() .items_center() .gap_2() .child(app_menu) - .child( - // Lithe 品牌标识 - h_flex() - .items_center() - .gap_1p5() - .pl_1() - .child( - Icon::new(IconName::Zap) - .size(px(16.0)) - .text_color(ThemeColors::primary()), - ) - .child( - div() - .font_weight(FontWeight::BOLD) - .text_sm() - .text_color(ThemeColors::foreground()) - .child("Lithe"), - ), - ) .child({ - // 项目选择器胶囊:Folder 图标 + 项目名 + ChevronDown,点击展开项目菜单 + // 项目菜单:logo + 项目名 + ChevronDown,点击展开项目列表 let v = view.clone(); let name = project_name.clone(); Button::new("tb-project-selector") .small() .ghost() - .rounded_md() - .bg(ThemeColors::surface()) - .border_1() - .border_color(ThemeColors::border()) .child( - Icon::new(IconName::Folder) - .size(px(13.0)) - .text_color(ThemeColors::muted_foreground()), + // 项目徽标:Tauri 用 public/logo.png,这里以同尺寸圆角方块承载品牌色。 + div() + .size(px(20.0)) + .flex_shrink_0() + .rounded(px(6.0)) + .bg(ThemeColors::primary()) + .flex() + .items_center() + .justify_center() + .child( + Icon::new(IconName::Zap) + .size(px(12.0)) + .text_color(ThemeColors::background()), + ), ) .child( div() + .max_w(px(224.0)) + .truncate() .text_xs() .text_color(ThemeColors::foreground()) .child(name.clone()), ) .child( Icon::new(IconName::ChevronDown) - .size(px(12.0)) + .size(px(14.0)) .text_color(ThemeColors::subtle_foreground()), ) .dropdown_menu(move |menu, _window, _cx| { @@ -454,21 +433,24 @@ impl Render for ToolbarView { || ToolbarEvent::NewProject, &v, )) - .item(event_item("Open Project", || ToolbarEvent::OpenProject, &v)) + .item(event_item("Open", || ToolbarEvent::OpenProject, &v)) .item(event_item( "Clone Repository", || ToolbarEvent::CloneRepository, &v, )) - .item(event_item("Open Recent", || ToolbarEvent::OpenRecent, &v)) + .separator() .item(event_item( "Open Projects", || ToolbarEvent::OpenProject, &v, )) .separator() - // 当前项目打勾标记,仅展示 - .item(PopupMenuItem::new(name.clone()).icon(IconName::CircleCheck)) + .item(event_item( + "Open Recent", + || ToolbarEvent::OpenRecent, + &v, + )) }) }) .child( @@ -500,168 +482,52 @@ impl Render for ToolbarView { ), ), ) - // 2. 中间:全局搜索条 - .child( - div() - .id("search-everywhere-bar") - .w(px(280.0)) - .h(px(28.0)) - .flex_shrink_0() - .bg(ThemeColors::surface()) - .border_1() - .border_color(ThemeColors::border()) - .rounded(px(6.0)) - .px_2() - .flex() - .items_center() - .justify_between() - .cursor_pointer() - .hover(|h| h.border_color(ThemeColors::primary())) - .on_click(cx.listener(|_this, _event, _window, cx| { - cx.emit(ToolbarEvent::QuickOpen); - })) - .child( - h_flex() - .items_center() - .gap_2() - .child( - Icon::new(IconName::Search) - .size(px(13.0)) - .text_color(ThemeColors::subtle_foreground()), - ) - .child( - div() - .text_xs() - .text_color(ThemeColors::subtle_foreground()) - .child("Search files, symbols..."), - ), - ) - .child( - div() - .text_xs() - .text_color(ThemeColors::subtle_foreground()) - .px_1p5() - .py(px(1.0)) - .bg(ThemeColors::background()) - .border_1() - .border_color(ThemeColors::border()) - .rounded(px(4.0)) - .child("Ctrl+P"), - ), - ) - // 3. 右侧工具组:运行目标、运行/调试/停止、主题、设置 + // 右侧:全局搜索图标按钮 + 窗口控件(对齐 Tauri `quickOpenAction` + `WindowControls`) .child( h_flex() - .flex_1() - .min_w_0() .items_center() - .justify_end() - .gap_2() - .child( - // 运行目标胶囊 - h_flex() - .items_center() - .gap_1p5() - .px_2() - .py(px(3.0)) - .rounded_md() - .bg(ThemeColors::surface()) - .border_1() - .border_color(ThemeColors::border()) - .child( - div() - .text_xs() - .text_color(ThemeColors::foreground()) - .child("[Project] Default"), - ) - .child( - Icon::new(IconName::ChevronDown) - .size(px(12.0)) - .text_color(ThemeColors::subtle_foreground()), - ), - ) - .child( - Button::new("tb-run") - .small() - .primary() - .icon(IconName::Play) - .label("Run") - .on_click(cx.listener(|_this, _event, _window, cx| { - cx.emit(ToolbarEvent::Run); - })), - ) - .child( - Button::new("tb-debug") - .small() - .ghost() - .icon(IconName::Bug) - .label("Debug") - .on_click(cx.listener(|_this, _event, _window, cx| { - cx.emit(ToolbarEvent::Debug); - })), - ) - .child( - Button::new("tb-stop") - .small() - .ghost() - .icon(IconName::Square) - .tooltip("Stop") - .on_click(cx.listener(|_this, _event, _window, cx| { - cx.emit(ToolbarEvent::Stop); - })), - ) + .flex_shrink_0() + .gap_1() .child( - // 主题切换:跟随系统时恒显示 Moon,否则按当前主题显示 Sun/Moon - Button::new("tb-theme-toggle") + Button::new("tb-quick-open") .small() .ghost() - .icon(theme_icon) - .tooltip("Toggle Theme") + .icon(IconName::Search) + .tooltip("Search") .on_click(cx.listener(|_this, _event, _window, cx| { - cx.emit(ToolbarEvent::ToggleTheme); + cx.emit(ToolbarEvent::QuickOpen); })), ) .child( - Button::new("tb-settings") - .small() - .ghost() - .icon(IconName::Settings) - .tooltip("Settings") - .on_click(cx.listener(|_this, _event, _window, cx| { - cx.emit(ToolbarEvent::OpenSettings); - })), + h_flex() + .items_center() + .flex_shrink_0() + .child(window_control( + "tb-window-minimize", + IconName::Minus, + false, + cx.listener(|_this, _event: &gpui_kit::ClickEvent, _window, cx| { + cx.emit(ToolbarEvent::WindowMinimize); + }), + )) + .child(window_control( + "tb-window-maximize", + IconName::Square, + false, + cx.listener(|_this, _event: &gpui_kit::ClickEvent, _window, cx| { + cx.emit(ToolbarEvent::WindowMaximize); + }), + )) + .child(window_control( + "tb-window-close", + IconName::Close, + true, + cx.listener(|_this, _event: &gpui_kit::ClickEvent, _window, cx| { + cx.emit(ToolbarEvent::WindowClose); + }), + )), ), ) - // 4. 窗口控件:46x40 直角按钮,Close 悬停使用 destructive - .child( - h_flex() - .items_center() - .flex_shrink_0() - .child(window_control( - "tb-window-minimize", - IconName::Minus, - false, - cx.listener(|_this, _event: &gpui_kit::ClickEvent, _window, cx| { - cx.emit(ToolbarEvent::WindowMinimize); - }), - )) - .child(window_control( - "tb-window-maximize", - IconName::Square, - false, - cx.listener(|_this, _event: &gpui_kit::ClickEvent, _window, cx| { - cx.emit(ToolbarEvent::WindowMaximize); - }), - )) - .child(window_control( - "tb-window-close", - IconName::Close, - true, - cx.listener(|_this, _event: &gpui_kit::ClickEvent, _window, cx| { - cx.emit(ToolbarEvent::WindowClose); - }), - )), - ) } } From a191e5f9527323c1765041c3004a99e5ccabd30f Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 13:08:12 +0800 Subject: [PATCH 09/59] =?UTF-8?q?fix(linux):=20=E5=B7=A6=E6=A0=8F=E5=8E=BB?= =?UTF-8?q?=E6=8E=89=20maven=E3=80=81=E7=BB=88=E7=AB=AF=E8=B7=9F=E9=9A=8F?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E3=80=81=E9=A1=B9=E7=9B=AE=E5=BE=BD=E6=A0=87?= =?UTF-8?q?=E6=8D=A2=E7=9C=9F=E5=AE=9E=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对照 Tauri 真源修复三处不一致: - 左侧活动栏去掉 maven:Tauri 左栏的 maven 项仅在 `hasMavenRun` 为真时出现(`main-sidebar.tsx` 有条件传入 `onMavenClick`), 默认无 Maven 运行故不显示;Maven 入口只保留在右侧插件栏 (`plugin-activity-rail.tsx` 的 `isMavenAvailable`)。 新增 `has_maven_run` 开关供后续 Maven 集成打开。 - 浅色模式终端仍深色:`terminal.rs` 硬编码深色底/文字/边框, 改为主题色(背景/前景/边框/柔和文字/成功色),对齐 Tauri xterm 取 `--background` / `--foreground` 的行为。 - 项目菜单触发器徽标:去掉蓝色闪电占位,改用与 Tauri `public/logo.png` 同一文件的真实应用图标(编译期嵌入, 20px 圆角展示)。 --- linux/assets/logo.png | Bin 0 -> 810582 bytes linux/src/workbench/activity_rail.rs | 42 +++++++++++++++++++++++---- linux/src/workbench/terminal.rs | 30 +++++++++++-------- linux/src/workbench/toolbar.rs | 24 +++++++++------ 4 files changed, 70 insertions(+), 26 deletions(-) create mode 100644 linux/assets/logo.png diff --git a/linux/assets/logo.png b/linux/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4f31230945c4502a5a1b31f8ecadc86221126932 GIT binary patch literal 810582 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%0AwIZEn~b<-Vqxn8634yBABoSA^vG9CMwO2 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

)cF8zBUuqey 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

ePUHRR{ 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˅TyzN)Xy{i1z+V^Ae;_MNoDDCJ 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

odT(x(Q^gSP?}&QZQ=?4Qq}4&QLT0LNZN^m zwjAklaB939X8tseVRas^2M##Hj0p!g!xwQSxCx8IM>wA@{)=59IP>w$4Ji^-gc2hDOD=jJ5QdQc!aWaV zJYAa0|rIS>DP(f;@C?dHLo7XQDz*_?a+#tCCSygEKVeesWOHgBfQ_3PRKkG%zK zrZ+mSU)L6Rye+V@%Xa_HvztHi>h8$B`|E| za`GTE(y#OCeOpqhK8N@8P5~Izl`mEOtzLYmiFI{lSzMPLkV67vdAuB~mIEL&xA;j< z=VC29Ixp3kab3U_1CI>U5!xEvQB1X}Xnb1t!1mRDw?k4C4=mIyBS=>$DqYL%RRC2b zBcjgGGpLUaBV+LeSX`3?C8G8!%qk)E?V!@5#aifVsf1j`|L7C;EXouVM99}u zmcS%SwX6zjF855#287$Y*RcKpwU1(+Q6Be+TXLsz!0x6zi!#3qchsIet1QNHQ zzIvwc)i&;&D4ZA^!O^50v$K7=CFzoi7jgya+W3ZymIvUb@@pO5n^PKA}e1Z(N z7)mkN6q?~|gwPX1xN|>7_p2~x535Y}xI+v*N`{BWcri^f3wc``^|W<>oB#O@&W!Q2 z$rj5B6sN6doQHml6hqqbBVVD?oG`H!wTT(40chS*W!66}YaRM40@7D^-g4ZnA!PN+ zcUFVsCA#un1T=s77hc#sCm;ug+Zc>Y<$WZraUAMHyeVz$y25tw+kS{zFD9U&T}trX zQlm9pWUIX)D&P<&r2vU_ax|?oi_NHRztnkm$Z1Vm%Y& zytOXr79uVk4GI-lwsgiWf{TM@X7L%l@s|d)aAt0s75$

TDf7-RA`eRr484Ay8A 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%0DM;Yh!E(aEV}%;F9$U>mTQKOZc$fy zGF0&|nDtL|(rXHA9*ljCVrok^cuIc;%<7Y^&mbgSr-}fiA{bbUQ~9VgK<9|k!#Y|F zjCh3E4&-Q#*6`B*;zMatIRN1pDT1?Xfm(qWfkwZxg3*Y`l{9>Vd9n2o3)hJ;PI!gH73(x4U`yi6QDATkL`6Etj*afH z@PZ9ZQ#FI?K@!zy65qU=zGfSn_=T zqC=8XVUCS*4JQeJ739haECBLzd4y?h7>Q7Ql1PS}IS3YRY*7e%2+$)zPzZ8IOgva$ zQMw5p(UumhXorG=6}>ouMS~K1?!%72nzJbCGV#f|AUgC!~tnQAuB=*N~2UX@sM(e ziS!z`xJ1cDlpO&L8yol?uj3>|SqfuF*w~w~M9Za#RvNrJKp0^$n3TZ_cR24zhmNg~ zb8@5bKm=Is9uHI{BL(3I5X7L70ie|-p_VAtYTZou@fUu^Axd?H?1qIB#9PN7Y;krg zqQRzA#qb{e#yld}hOAf}TiDsq1_DD{B2*3nSLQ?k2X9pZJ-`!RD8Ro;;-y$AOGa?P z!*B*X6Jm*9F*rc+k9NQx{qQTn+d~Si{^uWht<}n5DSS2=Da<9Q+oK*1fl}|#M4#i1 zP^AQ!yJX?XhoZa2C3G;0lWylJ;wwW$#~~`8 zehEc<>T$L;!yi9DgB^1c9&?o|u59h5SGIQKY9^_nzW7AfJ@~;%J9{yZCOoWVa?oZ6 zi|N^ocQ1~1Z-0;9FN!ih$7S@u=z-7A1MJnz?2XIlfzbn>w+A?TUfzdp|M)+AnFDQ$;C^p z-_O~dWGp3(XJ5|9o=!P`qR5|Buahzxb`F>mWccQnTyR{vms}O z)IVBe^4%OtD4E+ps7^w?w?LyoS|nNZ;wNVS)Va!wq(H3xr7o)y&dQ-CZgi#oBO_K) zzO1V)aJy}3W*y*FkK|cM(%k45p%$hA?C69{kQDKQMrc$Hk)L?Pv{JKB!kEU?VC4%G z0+f=O6O%AS4;ek&OuV2+8Z7aeUhFd|3lF9XA119gSr;Sxxv#?FpZG*3S~GY?8}SOA zt+x5Q;YJajaV&Ug7xH8=n8ELYqQnFq#~jM=$e^%d1D~2&N+Hw)qfr%~1*n4tpJODq z@!|{-_6ZkF%C~mQogB=}d<$z*KJ@@Hy zS~MDvx*(wrc;g{oTgHA3b$SB(wM_@;5OhP$q)9n~P754<%FoFb`9lTv`(41*llZ* z2T$M8Za?$C!7#@sHhA1{HwZrxiEsy6v~-9vTW-48kX=)MmKAiVObQJ35Jhlj7@?=t z1NjgXZprPK4U;K3%%@!`1|NZJz0MYt~xckDgTI7TLeHOOpYYXS+_Yb6^^eVCKva5n`4FfPDDh)%<$j$(M8c{|hh)hI0?L z_TWbv&qv~1A8vB=_1=-TIqv z>o!gvYm-YC`+k=)-S6F|yypx(oRlf5SmU1)ZSo%>sor5Bex5 zV6sytqyk8*OhP1114!9SssoXl$xY%k>=vLVN0WC^*pQSOnI^^}R;gmeU+d9p+mNkA zg~UJwseS{I7N8gL3>|67iF0bjQPKQcdl=b=4J4jWqYP?b{*!V

j+6`!L37I>imlI1JI;01muwGSX@wgy6J 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, /// 底部组项顺序,按 `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 {} @@ -52,6 +59,23 @@ 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; @@ -61,6 +85,7 @@ impl ActivityRailView { .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 常量顺序取交集,保证与参考实现一致。 @@ -74,13 +99,20 @@ impl ActivityRailView { .copied() .filter(|id| visible.contains(id)) .collect(); + (top_items, bottom_items) + } - Self { - active_view: Some("files".to_string()), - active_bottom: None, - 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) { diff --git a/linux/src/workbench/terminal.rs b/linux/src/workbench/terminal.rs index 090eaa3cb..bb3dca67c 100644 --- a/linux/src/workbench/terminal.rs +++ b/linux/src/workbench/terminal.rs @@ -2,7 +2,7 @@ use gpui_kit::component::button::{Button, ButtonVariants as _}; use gpui_kit::component::scroll::ScrollableElement as _; use gpui_kit::component::{h_flex, v_flex, Sizable as _}; use gpui_kit::{ - div, px, rgb, Context, InteractiveElement as _, IntoElement, ParentElement as _, Render, + div, px, Context, InteractiveElement as _, IntoElement, ParentElement as _, Render, Styled as _, Window, }; use portable_pty::{native_pty_system, CommandBuilder, PtySize}; @@ -10,6 +10,8 @@ use std::io::{Read, Write}; use std::sync::{Arc, Mutex}; use std::thread; +use crate::theme::ThemeColors; + /// Linux 原生 PTY 会话 pub struct TerminalSession { writer: Arc>>, @@ -141,7 +143,9 @@ impl Render for TerminalView { v_flex() .size_full() - .bg(rgb(0x0e0f17)) + // 终端区背景跟随主题(对齐 Tauri xterm 的 `--background`), + // 浅色主题下不再残留深色底。 + .bg(ThemeColors::background()) .p_2() .child( // 终端顶部控制栏 @@ -151,14 +155,14 @@ impl Render for TerminalView { .justify_between() .items_center() .border_b_1() - .border_color(rgb(0x23263b)) + .border_color(ThemeColors::border()) .pb_1() .child( h_flex() .items_center() .gap_2() .text_xs() - .text_color(rgb(0x8a90a2)) + .text_color(ThemeColors::subtle_foreground()) .child("Linux PTY Shell") .child(format!("({})", self.working_dir)), ) @@ -180,12 +184,14 @@ impl Render for TerminalView { .overflow_y_scrollbar() .p_2() .text_xs() - .text_color(rgb(0xd1d5db)) + .text_color(ThemeColors::foreground()) .font_family("monospace") .children(lines.into_iter().enumerate().map(|(idx, line)| { - div() - .id(idx) - .child(if line.is_empty() { " ".to_string() } else { line }) + div().id(idx).child(if line.is_empty() { + " ".to_string() + } else { + line + }) })), ) .child( @@ -193,16 +199,16 @@ impl Render for TerminalView { h_flex() .h(px(32.0)) .w_full() - .bg(rgb(0x141522)) + .bg(ThemeColors::surface()) .border_t_1() - .border_color(rgb(0x23263b)) + .border_color(ThemeColors::border()) .items_center() .px_2() .gap_2() .child( div() .text_xs() - .text_color(rgb(0x10b981)) + .text_color(ThemeColors::success()) .font_family("monospace") .child("$"), ) @@ -210,7 +216,7 @@ impl Render for TerminalView { div() .flex_1() .text_xs() - .text_color(rgb(0xffffff)) + .text_color(ThemeColors::foreground()) .font_family("monospace") .child(if self.current_input.is_empty() { "Type command here...".to_string() diff --git a/linux/src/workbench/toolbar.rs b/linux/src/workbench/toolbar.rs index ec2e162ae..f34013cbe 100644 --- a/linux/src/workbench/toolbar.rs +++ b/linux/src/workbench/toolbar.rs @@ -50,11 +50,16 @@ pub enum ToolbarEvent { OpenRecent, } +/// 应用图标:与 Tauri 端 `public/logo.png` 同一文件,编译期嵌入。 +const APP_LOGO_PNG: &[u8] = include_bytes!("../../assets/logo.png"); + pub struct ToolbarView { pub workspace_name: String, pub git_branch: Option, /// 紧凑菜单条是否展开(对齐 Tauri `isCompactMenuVisible`)。 compact_menu_open: bool, + /// 解码后的应用图标,项目菜单触发器左侧的徽标(对齐 Tauri 的 `logo.png`)。 + app_logo: std::sync::Arc, } impl EventEmitter for ToolbarView {} @@ -70,6 +75,10 @@ impl ToolbarView { workspace_name: name, git_branch: None, compact_menu_open: false, + app_logo: std::sync::Arc::new(gpui_kit::Image::from_bytes( + gpui_kit::ImageFormat::Png, + APP_LOGO_PNG.to_vec(), + )), } } @@ -392,26 +401,23 @@ impl Render for ToolbarView { .gap_2() .child(app_menu) .child({ - // 项目菜单:logo + 项目名 + ChevronDown,点击展开项目列表 + // 项目菜单:应用图标 + 项目名 + ChevronDown,点击展开项目列表 let v = view.clone(); let name = project_name.clone(); + let logo = self.app_logo.clone(); Button::new("tb-project-selector") .small() .ghost() .child( - // 项目徽标:Tauri 用 public/logo.png,这里以同尺寸圆角方块承载品牌色。 + // 项目徽标:Tauri 用 public/logo.png(size-5 圆角),保持一致。 div() .size(px(20.0)) .flex_shrink_0() .rounded(px(6.0)) - .bg(ThemeColors::primary()) - .flex() - .items_center() - .justify_center() + .overflow_hidden() .child( - Icon::new(IconName::Zap) - .size(px(12.0)) - .text_color(ThemeColors::background()), + gpui_kit::img(gpui_kit::ImageSource::Image(logo)) + .size_full(), ), ) .child( From b218703e82e2682a56dae254161b792f6bc50390 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 13:59:14 +0800 Subject: [PATCH 10/59] =?UTF-8?q?fix(linux):=20=E7=A9=BA=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8=E5=AF=B9=E9=BD=90=20Tauri=20EmptyEditorState?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原空态是自创的 Zap 大图标 + "Lithe IDEA" + 英文介绍 + 快捷键提示, Tauri 不存在。改为复刻 `empty-editor-state.tsx`: 文件图标(40px)+ 右下角放大镜叠加(20px),标题「选择文件以查看」, 描述「外部工具产生的更改会自动显示。」(与 `locale.ts` 中文文案逐字一致)。 --- linux/src/workbench/editor.rs | 102 +++++++++++++--------------------- 1 file changed, 40 insertions(+), 62 deletions(-) diff --git a/linux/src/workbench/editor.rs b/linux/src/workbench/editor.rs index cd505bc84..41db6953e 100644 --- a/linux/src/workbench/editor.rs +++ b/linux/src/workbench/editor.rs @@ -48,12 +48,15 @@ pub struct EditorView { impl EditorView { pub fn new(workspace_root: String, window: &mut Window, cx: &mut Context) -> Self { let editor_state = cx.new(|cx| EditorState::new(window, cx).line_number(true)); - let _editor_subscription = - cx.subscribe_in(&editor_state, window, |this, _state, event: &InputEvent, _window, cx| { + 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, @@ -420,85 +423,60 @@ impl Render for EditorView { .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), - ) - }) + .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), + ) + }, + ) .when(active_tab.is_none(), |this| { - // 无打开文件 Empty State(精致 Lithe 居中徽标与操作提示) + // 无打开文件空态,对齐 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( - Icon::new(IconName::Zap) + div() + .relative() .size(px(48.0)) - .text_color(ThemeColors::accent_blue()), + .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_lg() - .font_weight(FontWeight::BOLD) + .text_base() + .font_weight(FontWeight::MEDIUM) .text_color(ThemeColors::text_primary()) - .child("Lithe IDEA"), + .child("选择文件以查看"), ) .child( div() - .text_xs() + .text_sm() .text_color(ThemeColors::text_muted()) - .child("Next-generation IDE for Linux, powered by GPUI Kit & Rust Core"), - ) - .child( - v_flex() - .pt_2() - .gap_2() - .child( - h_flex() - .items_center() - .gap_2() - .text_xs() - .text_color(ThemeColors::text_muted()) - .child("Search files everywhere:") - .child( - div() - .px_1p5() - .py(px(1.0)) - .bg(ThemeColors::bg_tab_hover()) - .border_1() - .border_color(ThemeColors::border()) - .rounded_sm() - .text_color(ThemeColors::text_primary()) - .child("Ctrl+P"), - ), - ) - .child( - h_flex() - .items_center() - .gap_2() - .text_xs() - .text_color(ThemeColors::text_muted()) - .child("Toggle terminal panel:") - .child( - div() - .px_1p5() - .py(px(1.0)) - .bg(ThemeColors::bg_tab_hover()) - .border_1() - .border_color(ThemeColors::border()) - .rounded_sm() - .text_color(ThemeColors::text_primary()) - .child("Ctrl+`"), - ), - ), + .child("外部工具产生的更改会自动显示。"), ), ), ) From 38523f4b67dd68e78a512f9b6596f199087d5c3c Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 14:06:37 +0800 Subject: [PATCH 11/59] =?UTF-8?q?fix(linux):=20=E8=AE=BE=E7=BD=AE=E5=88=86?= =?UTF-8?q?=E7=B1=BB=E6=A0=87=E9=A2=98=E4=B8=8E=20Tauri=20=E4=B8=AD?= =?UTF-8?q?=E6=96=87=E6=96=87=E6=A1=88=E9=80=90=E5=AD=97=E5=AF=B9=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 逐项核对 `settings-dialog.tsx` 各分类 `labelKey` 在 `locale.ts` / `ai-commit.ts` 中的中文翻译,修正 8 处自创标题: 常规(原通用)、项目 · JDK 与 Maven(原项目)、运行配置(原运行)、 LSP(原语言服务)、AI 聊天与编辑(原 AI 助手)、AI 与提交(原 AI 提交)、 Git(原版本控制)。编辑器/快捷键/终端/日志/更新原本已一致。 --- linux/src/workbench/settings_dialog.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/linux/src/workbench/settings_dialog.rs b/linux/src/workbench/settings_dialog.rs index 155297809..9214a130e 100644 --- a/linux/src/workbench/settings_dialog.rs +++ b/linux/src/workbench/settings_dialog.rs @@ -73,19 +73,19 @@ impl SettingsCategory { } } - /// 中文标题,展示在导航项与内容区标题。 + /// 中文标题,与 Tauri `settings-dialog.tsx` 各分类 `labelKey` 的中文翻译逐字对应。 pub fn title(self) -> &'static str { match self { - SettingsCategory::General => "通用", - SettingsCategory::Project => "项目", - SettingsCategory::Run => "运行", + SettingsCategory::General => "常规", + SettingsCategory::Project => "项目 · JDK 与 Maven", + SettingsCategory::Run => "运行配置", SettingsCategory::Editor => "编辑器", SettingsCategory::Keyboard => "快捷键", SettingsCategory::Terminal => "终端", - SettingsCategory::Lsp => "语言服务", - SettingsCategory::Ai => "AI 助手", - SettingsCategory::AiCommit => "AI 提交", - SettingsCategory::Git => "版本控制", + SettingsCategory::Lsp => "LSP", + SettingsCategory::Ai => "AI 聊天与编辑", + SettingsCategory::AiCommit => "AI 与提交", + SettingsCategory::Git => "Git", SettingsCategory::Logs => "日志", SettingsCategory::Updates => "更新", } @@ -109,7 +109,7 @@ impl SettingsCategory { } } - /// 由持久化的 `lastSettingsTab` 反解分类;`language` 归到 LSP,未知值回退通用。 + /// 由持久化的 `lastSettingsTab` 反解分类;`language` 归到 LSP,未知值回退常规。 pub fn from_id(id: &str) -> Self { match id { "general" => SettingsCategory::General, @@ -461,7 +461,7 @@ impl Render for SettingsDialog { } } -/// 通用渲染块:分组、行、控件与分类内容。 +/// 常规渲染块:分组、行、控件与分类内容。 impl SettingsDialog { /// 左侧导航项:active 使用 accent 背景 + 左侧 primary 竖条 + 加粗前景。 fn render_nav_item(&self, cat: SettingsCategory, cx: &mut Context) -> impl IntoElement { From 44688b2d96f869d3be6797c1f3dafa669cb85eeb Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 14:39:20 +0800 Subject: [PATCH 12/59] =?UTF-8?q?feat(linux):=20=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=E5=86=85=E5=AE=B9=E4=B8=8E=E5=9B=BE=E6=A0=87?= =?UTF-8?q?=E5=AE=8C=E5=85=A8=E5=AF=B9=E9=BD=90=20Tauri?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 图标(对照 settings-dialog.tsx): general→Settings、run→Cog、terminal→SquareTerminal、 ai/ai-commit→WandSparkles、git→Code(与编辑器同图标,和 Tauri 一致)。 面板内容对照 macos-settings-panels.tsx 各 Panel 逐项重写: - 常规:外观(配色主题/外观模式下拉)、语言、项目打开方式、 自动保存、Git 保护策略、隐藏路径(目录/文件模式展示+应用)。 - 编辑器:显示(字号步进 10-22、用法与作者开关)、标签页 (缓冲区轮播)、缩进(制表符宽度下拉)。 - 快捷键:预设下拉(Lithe/VSCode/JetBrains/Xcode)+ 快捷键搜索框。 - 终端:Shell 分组默认 Shell 下拉(系统默认/PowerShell/命令提示符/WSL)。 - LSP:自动补全/参数提示/语义高亮 + 已检测服务器说明。 - AI/AI 提交:分组结构与说明(完整配置后续接入)。 - Git:Fetch 默认行为/集成/视图/默认差异视图/编辑器全量偏好项。 - 日志:位置/诊断/保留策略/诊断包分组结构。 - 更新:软件更新分组(版本+检查按钮+状态文案)。 - 项目:JDK/Maven 三字段结构;运行:描述+生成按钮。 Settings 新增 24 个持久化字段,键与默认值与 Tauri defaultSettings 一致;新增下拉框/多行文本展示控件。 --- linux/src/settings.rs | 85 ++ linux/src/workbench/settings_dialog.rs | 1551 +++++++++++++----------- 2 files changed, 913 insertions(+), 723 deletions(-) diff --git a/linux/src/settings.rs b/linux/src/settings.rs index e3dfdd291..75f3f5105 100644 --- a/linux/src/settings.rs +++ b/linux/src/settings.rs @@ -86,6 +86,32 @@ impl Default for CoreFeatures { } } +/// 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", +]; + /// 应用设置真源,字段名与 Tauri `defaultSettings` 的 camelCase 键一一对应。 #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] @@ -99,8 +125,10 @@ pub struct Settings { pub word_wrap: bool, pub line_numbers: bool, pub show_minimap: bool, + pub code_lens: bool, // Terminal pub terminal_scrollback: usize, + pub terminal_default_shell_id: String, // UI pub display_language: String, pub reduce_motion: bool, @@ -127,6 +155,33 @@ pub struct Settings { // Tabs pub max_open_tabs: usize, pub horizontal_tab_scroll: bool, + // Keyboard + pub keybinding_preset: String, + // Language + 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, + // File tree + pub hidden_file_patterns: Vec, + pub hidden_directory_patterns: Vec, + // 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, // Advanced pub last_settings_tab: String, pub core_features: CoreFeatures, @@ -142,7 +197,9 @@ impl Default for Settings { word_wrap: false, line_numbers: true, show_minimap: true, + code_lens: true, terminal_scrollback: 10000, + terminal_default_shell_id: String::new(), display_language: "zh-CN".to_string(), reduce_motion: false, show_status_bar: true, @@ -174,6 +231,34 @@ impl Default for Settings { right_tool_window_width: 400.0, max_open_tabs: 100, horizontal_tab_scroll: true, + keybinding_preset: "none".to_string(), + auto_completion: true, + parameter_hints: true, + semantic_tokens: true, + open_folders_in_new_window: true, + ask_where_to_open_projects: true, + 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(), + 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(), last_settings_tab: "general".to_string(), core_features: CoreFeatures::default(), } diff --git a/linux/src/workbench/settings_dialog.rs b/linux/src/workbench/settings_dialog.rs index 9214a130e..fe09ae4c5 100644 --- a/linux/src/workbench/settings_dialog.rs +++ b/linux/src/workbench/settings_dialog.rs @@ -9,6 +9,7 @@ use gpui_kit::assets::IconName; use gpui_kit::component::button::{Button, ButtonVariants as _}; +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 _; @@ -91,19 +92,21 @@ impl SettingsCategory { } } - /// 导航图标;只使用 `IconName` 中确定存在的变体。 + /// 导航图标;与 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::SlidersHorizontal, + SettingsCategory::General => IconName::Settings, SettingsCategory::Project => IconName::Folder, - SettingsCategory::Run => IconName::Play, + SettingsCategory::Run => IconName::Cog, SettingsCategory::Editor => IconName::Code, SettingsCategory::Keyboard => IconName::Keyboard, - SettingsCategory::Terminal => IconName::Terminal, + SettingsCategory::Terminal => IconName::SquareTerminal, SettingsCategory::Lsp => IconName::Database, - SettingsCategory::Ai => IconName::Bot, - SettingsCategory::AiCommit => IconName::Bot, - SettingsCategory::Git => IconName::GitBranch, + SettingsCategory::Ai => IconName::WandSparkles, + SettingsCategory::AiCommit => IconName::WandSparkles, + SettingsCategory::Git => IconName::Code, SettingsCategory::Logs => IconName::FileText, SettingsCategory::Updates => IconName::RotateCw, } @@ -142,20 +145,10 @@ pub struct SettingsDialog { pub active_category: SettingsCategory, /// 当前工作区根路径,Project 分类只读展示 workspace_root: String, - // 以下字段对应 Tauri 设置但 Linux `Settings` 尚未提供,暂作对话框本地占位。 - open_folders_in_new_window: bool, - keybinding_preset: String, - vim_mode: bool, - vim_relative_line_numbers: bool, - terminal_cursor_style: String, - lsp_auto_completion: bool, - lsp_parameter_hints: bool, - auto_refresh_git_status: bool, - show_untracked_files: bool, - show_staged_first: bool, - git_default_diff_view: String, - enable_inline_git_blame: bool, - log_level: String, + /// Git 本地更改保护策略(对齐 Tauri `GeneralPanel` 的本地 `gitPolicy` state,暂不落盘) + git_policy: String, + /// 本次会话诊断日志开关(对齐 Tauri 日志面板的会话级状态,重启恢复默认) + diagnostic_mode: bool, } impl EventEmitter for SettingsDialog {} @@ -172,19 +165,8 @@ impl SettingsDialog { Self { active_category, workspace_root, - open_folders_in_new_window: true, - keybinding_preset: "none".to_string(), - vim_mode: false, - vim_relative_line_numbers: false, - terminal_cursor_style: "bar".to_string(), - lsp_auto_completion: true, - lsp_parameter_hints: true, - auto_refresh_git_status: true, - show_untracked_files: true, - show_staged_first: true, - git_default_diff_view: "unified".to_string(), - enable_inline_git_blame: true, - log_level: "info".to_string(), + git_policy: "ask".to_string(), + diagnostic_mode: false, } } @@ -201,21 +183,10 @@ impl SettingsDialog { cx.notify(); } - /// 恢复占位项的本地默认值(不涉及持久化)。 + /// 恢复本地占位项的默认值(不涉及持久化)。 fn reset_placeholders(&mut self) { - self.open_folders_in_new_window = true; - self.keybinding_preset = "none".to_string(); - self.vim_mode = false; - self.vim_relative_line_numbers = false; - self.terminal_cursor_style = "bar".to_string(); - self.lsp_auto_completion = true; - self.lsp_parameter_hints = true; - self.auto_refresh_git_status = true; - self.show_untracked_files = true; - self.show_staged_first = true; - self.git_default_diff_view = "unified".to_string(); - self.enable_inline_git_blame = true; - self.log_level = "info".to_string(); + self.git_policy = "ask".to_string(); + self.diagnostic_mode = false; } /// 写入设置、广播 `Changed` 并刷新视图。 @@ -529,11 +500,12 @@ impl SettingsDialog { .child(v_flex().w_full().gap_3().p_3().child(content)) } - /// 设置行:左侧标签与描述,右侧控件。 + /// 设置行:左侧标签与描述,右侧控件。标签与描述使用 owned 字符串, + /// 以便展示动态值(主题名、路径、版本号等)。 fn render_row( &self, - label: &'static str, - description: Option<&'static str>, + label: String, + description: Option, control: impl IntoElement, ) -> impl IntoElement { let mut info = v_flex().flex_1().min_w_0().child( @@ -601,44 +573,6 @@ impl SettingsDialog { .on_click(cx.listener(move |this, _event, _window, cx| toggle(this, cx))) } - /// 单选项:分段药丸,选中态用 primary 背景。 - fn render_segment( - &self, - id: &'static str, - label: &'static str, - selected: bool, - cx: &mut Context, - on_select: impl Fn(&mut Self, &mut Context) + 'static, - ) -> impl IntoElement { - h_flex() - .id(id) - .items_center() - .justify_center() - .px_2p5() - .py_1() - .rounded_sm() - .border_1() - .cursor_pointer() - .text_xs() - .when(selected, |el| { - el.bg(ThemeColors::primary()) - .border_color(ThemeColors::primary()) - .text_color(ThemeColors::foreground()) - .font_weight(FontWeight::MEDIUM) - }) - .when(!selected, |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(label) - .on_click(cx.listener(move |this, _event, _window, cx| on_select(this, cx))) - } - /// 数值步进器:减号 + 当前值 + 加号。 fn render_stepper( &self, @@ -694,39 +628,80 @@ impl SettingsDialog { .child(value) } - /// 键位徽标:Keyboard 分类展示用,点击不触发录制。 - fn render_key_badge(&self, label: &'static str) -> impl IntoElement { - div() - .px_2() - .py_0p5() - .rounded_sm() + /// 下拉选择框:当前值按钮 + ChevronDown,点击展开选项列表。 + /// 对齐 Tauri 各面板的原生 ``(`controlClassName` + `w-40/32/44`)。 - /// `options` 为 (值, 展示文本) 对,按钮显示当前值对应的展示文本,选中项打勾。 + /// `options` 为 (值, 展示文本) 对,展示文本为 owned String(便于动态拼接如 "2 个空格"), + /// 按钮显示当前值对应的展示文本,选中项打勾。 fn render_dropdown( &self, id: &'static str, current: String, width: f32, - options: &'static [(&'static str, &'static str)], + options: Vec<(&'static str, String)>, cx: &mut Context, on_select: impl Fn(&mut Self, &'static str, &mut Context) + 'static, ) -> impl IntoElement { let view = cx.entity(); let on_select = std::rc::Rc::new(on_select); + let current_owned = current.clone(); + let options_for_label = options.clone(); // 按钮展示当前值对应的展示文本(对齐原生 select 显示 label 的行为)。 - let current_label = options + let current_label = options_for_label .iter() - .find(|(value, _)| *value == current.as_str()) - .map(|(_, label)| label.to_string()) - .unwrap_or(current.clone()); + .find(|(value, _)| *value == current_owned.as_str()) + .map(|(_, label)| label.clone()) + .unwrap_or(current_owned.clone()); Button::new(id) .small() .ghost() @@ -670,18 +686,20 @@ impl SettingsDialog { ) .dropdown_menu(move |menu, _window, _cx| { let mut menu = menu; - for (value, label) in options { + for (value, label) in &options { let v = view.clone(); let select = on_select.clone(); - let selected = *value == current.as_str(); - let item = gpui_kit::component::menu::PopupMenuItem::new(*label); + let value = *value; + let label = label.clone(); + let selected = value == current.as_str(); + 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| select(this, *value, cx)); + v.update(cx, |this, cx| select(this, value, cx)); })); } menu @@ -705,7 +723,7 @@ impl SettingsDialog { } /// 分组内的说明文本。 - fn render_note(&self, text: &'static str) -> impl IntoElement { + fn render_note(&self, text: String) -> impl IntoElement { div() .text_xs() .text_color(ThemeColors::subtle_foreground()) @@ -742,18 +760,29 @@ impl SettingsDialog { .gap_4() .child( self.render_group( - "外观", + crate::i18n::menu_text(cx, "settings.mac.appearance").to_string(), v_flex() .w_full() .gap_3() .child(self.render_row( - "配色主题".to_string(), + crate::i18n::menu_text(cx, "settings.mac.colorTheme").to_string(), None, self.render_dropdown( "general-theme", s.theme.clone(), 160.0, - &[("lithe-dark", "深色"), ("lithe-light", "浅色")], + vec![ + ( + "lithe-dark", + crate::i18n::menu_text(cx, "settings.mac.dark") + .to_string(), + ), + ( + "lithe-light", + crate::i18n::menu_text(cx, "settings.mac.light") + .to_string(), + ), + ], cx, |this, theme_id, cx| { // 跟随系统时写入对应的自动主题,否则直接切换主题。 @@ -772,85 +801,134 @@ impl SettingsDialog { }, ), )) - .child(self.render_row( - "外观模式".to_string(), - Some("选择配色主题,并设置是否跟随系统外观。".to_string()), - self.render_dropdown( - "general-appearance-mode", - appearance_mode, - 160.0, - &[("system", "跟随系统"), ("light", "浅色"), ("dark", "深色")], - cx, - |this, mode, cx| { - this.commit_theme(cx, |s| { - if mode == "system" { - s.sync_system_theme = true; - } else { - s.sync_system_theme = false; - s.theme = if mode == "light" { - "lithe-light".to_string() + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.mac.appearanceMode") + .to_string() + .to_string(), + Some( + crate::i18n::menu_text( + cx, + "settings.mac.appearanceDescription", + ) + .to_string(), + ), + self.render_dropdown( + "general-appearance-mode", + appearance_mode, + 160.0, + vec![ + ( + "system", + crate::i18n::menu_text(cx, "settings.mac.followSystem") + .to_string(), + ), + ( + "light", + crate::i18n::menu_text(cx, "settings.mac.light") + .to_string(), + ), + ( + "dark", + crate::i18n::menu_text(cx, "settings.mac.dark") + .to_string(), + ), + ], + cx, + |this, mode, cx| { + this.commit_theme(cx, |s| { + if mode == "system" { + s.sync_system_theme = true; } else { - "lithe-dark".to_string() - }; - } - }) - }, + s.sync_system_theme = false; + s.theme = if mode == "light" { + "lithe-light".to_string() + } else { + "lithe-dark".to_string() + }; + } + }) + }, + ), ), - )), + ), ), ) .child(self.render_group( - "语言", + crate::i18n::menu_text(cx, "settings.mac.language").to_string(), v_flex().w_full().gap_3().child(self.render_row( - "语言".to_string(), - Some("界面语言会立即生效。默认语言为英文。".to_string()), + crate::i18n::menu_text(cx, "settings.mac.language").to_string(), + Some( + crate::i18n::menu_text(cx, "settings.mac.languageDescription").to_string(), + ), self.render_dropdown( "general-language", s.display_language.clone(), 160.0, - &[("en-US", "English"), ("zh-CN", "简体中文")], + vec![ + ("en-US", "English".to_string()), + ("zh-CN", "简体中文".to_string()), + ], cx, |this, lang, cx| this.commit(cx, |s| s.display_language = lang.to_string()), ), )), )) - .child(self.render_group( - "项目", - v_flex().w_full().gap_3().child(self.render_row( - "项目打开方式".to_string(), - Some( - "选择打开其他项目时是每次询问、保留在此窗口,还是创建新窗口。".to_string(), - ), - self.render_dropdown( - "general-project-placement", - placement, - 160.0, - &[ - ("ask", "每次询问"), - ("this-window", "此窗口"), - ("new-window", "新窗口"), - ], - cx, - |this, mode, cx| { - this.commit(cx, |s| match mode { - "ask" => s.ask_where_to_open_projects = true, - "this-window" => { - s.ask_where_to_open_projects = false; - s.open_folders_in_new_window = false; - } - _ => { - s.ask_where_to_open_projects = false; - s.open_folders_in_new_window = true; - } - }) - }, + .child( + self.render_group( + crate::i18n::menu_text(cx, "settings.mac.projects").to_string(), + v_flex().w_full().gap_3().child( + self.render_row( + crate::i18n::menu_text(cx, "settings.mac.openProjectsIn").to_string(), + Some( + crate::i18n::menu_text(cx, "settings.mac.openProjectsDescription") + .to_string() + .to_string(), + ), + self.render_dropdown( + "general-project-placement", + placement, + 160.0, + vec![ + ( + "ask", + crate::i18n::menu_text(cx, "settings.mac.askEveryTime") + .to_string(), + ), + ( + "this-window", + crate::i18n::menu_text(cx, "settings.mac.thisWindow") + .to_string(), + ), + ( + "new-window", + crate::i18n::menu_text(cx, "settings.mac.newWindow") + .to_string(), + ), + ], + cx, + |this, mode, cx| { + this.commit(cx, |s| match mode { + "ask" => s.ask_where_to_open_projects = true, + "this-window" => { + s.ask_where_to_open_projects = false; + s.open_folders_in_new_window = false; + } + _ => { + s.ask_where_to_open_projects = false; + s.open_folders_in_new_window = true; + } + }) + }, + ), + ), ), - )), - )) + ), + ) .child(self.render_group( - "文件", + crate::i18n::menu_text(cx, "settings.mac.files").to_string(), v_flex().w_full().gap_3().child(self.render_row( - "自动保存更改的文件".to_string(), + crate::i18n::menu_text(cx, "settings.mac.autoSave").to_string(), None, self.render_toggle("general-auto-save", s.auto_save, cx, |this, cx| { this.commit(cx, |s| s.auto_save = !s.auto_save) @@ -858,18 +936,29 @@ impl SettingsDialog { )), )) .child(self.render_group( - "Git", + crate::i18n::menu_text(cx, "settings.tabs.git").to_string(), v_flex().w_full().gap_3().child(self.render_row( - "保存本地更改的方式".to_string(), - Some("选择执行 Git 操作前保护本地更改的方式。".to_string()), + crate::i18n::menu_text(cx, "settings.mac.saveLocalChangesWith").to_string(), + Some( + crate::i18n::menu_text(cx, "settings.mac.gitPolicyDescription").to_string(), + ), self.render_dropdown( "general-git-policy", self.git_policy.clone(), 160.0, - &[ - ("ask", "每次询问"), - ("shelf", "暂存架"), - ("stash", "Git 贮藏"), + vec![ + ( + "ask", + crate::i18n::menu_text(cx, "settings.mac.askEveryTime").to_string(), + ), + ( + "shelf", + crate::i18n::menu_text(cx, "settings.mac.shelf").to_string(), + ), + ( + "stash", + crate::i18n::menu_text(cx, "settings.mac.gitStash").to_string(), + ), ], cx, |this, policy, cx| { @@ -881,35 +970,35 @@ impl SettingsDialog { )) .child( self.render_group( - "隐藏路径", + crate::i18n::menu_text(cx, "settings.mac.hiddenPaths").to_string(), v_flex() .w_full() .gap_3() - .child(self.render_note( - "每行一项。目录名称会隐藏匹配的文件夹;文件条目支持 * 和 ?。", - )) .child( - v_flex().w_full().gap_1p5().child( - div() - .text_xs() - .text_color(ThemeColors::foreground()) - .child("目录"), + self.render_note( + crate::i18n::menu_text(cx, "settings.mac.hiddenPathsDescription") + .to_string(), ), ) + .child(v_flex().w_full().gap_1p5().child( + div().text_xs().text_color(ThemeColors::foreground()).child( + crate::i18n::menu_text(cx, "settings.mac.directories").to_string(), + ), + )) .child(self.render_text_block(s.hidden_directory_patterns.join("\n"), 72.0)) - .child( - div() - .text_xs() - .text_color(ThemeColors::foreground()) - .child("文件模式"), - ) + .child(div().text_xs().text_color(ThemeColors::foreground()).child( + crate::i18n::menu_text(cx, "settings.mac.filePatterns").to_string(), + )) .child(self.render_text_block(s.hidden_file_patterns.join("\n"), 56.0)) .child( h_flex().w_full().justify_end().child( Button::new("general-apply-patterns") .small() .primary() - .label("应用") + .label( + crate::i18n::menu_text(cx, "settings.mac.apply") + .to_string(), + ) .on_click(cx.listener(|_this, _event, _window, cx| { // 模式编辑器后续接入;当前落盘值即显示值。 cx.notify(); @@ -934,36 +1023,42 @@ impl SettingsDialog { .text_color(ThemeColors::foreground()) .child(self.workspace_root.clone()), ) - .child(self.render_note( - "仅保存在当前电脑,作用于当前项目。运行配置默认继承这些值,单独设置的覆盖值保持不变。路径留空时使用自动选择。", - )) + .child( + self.render_note(crate::i18n::menu_text(cx, "settings.project.scope").to_string()), + ) .child( self.render_group( - "工具链", + crate::i18n::menu_text(cx, "settings.project.toolchain").to_string(), v_flex() .w_full() .gap_3() .child(self.render_row( - "JDK 主目录".to_string(), - Some("自动检测(留空)".to_string()), - self.render_value(String::new()), - )) - .child(self.render_row( - "Maven 主目录 / 可执行文件".to_string(), - Some("自动检测(留空)".to_string()), + crate::i18n::menu_text(cx, "run.jdkHome").to_string(), + Some(crate::i18n::menu_text(cx, "run.toolchainAuto").to_string()), self.render_value(String::new()), )) .child(self.render_row( - "Maven JDK 主目录".to_string(), - Some("使用项目 JDK".to_string()), + crate::i18n::menu_text(cx, "run.mavenExecutable").to_string(), + Some(crate::i18n::menu_text(cx, "run.toolchainAuto").to_string()), self.render_value(String::new()), )) + .child( + self.render_row( + crate::i18n::menu_text(cx, "run.mavenJdkHome").to_string(), + Some( + crate::i18n::menu_text(cx, "settings.project.useProjectJdk") + .to_string() + .to_string(), + ), + self.render_value(String::new()), + ), + ) .child( h_flex().w_full().justify_end().child( Button::new("project-save") .small() .primary() - .label("保存") + .label(crate::i18n::menu_text(cx, "ui.save").to_string()) .on_click(cx.listener(|_this, _event, _window, cx| { // 工具链探测与保存尚未接入后端。 cx.notify(); @@ -978,19 +1073,21 @@ impl SettingsDialog { fn render_run_content(&self, cx: &mut Context) -> impl IntoElement { v_flex().w_full().gap_4().child( self.render_group( - "运行配置", + crate::i18n::menu_text(cx, "settings.run.title").to_string(), v_flex() .w_full() .gap_3() .child(self.render_note( - "选择服务或任务,配置启动参数、环境变量和项目环境覆盖项;点击保存后生效。", + crate::i18n::menu_text(cx, "settings.run.description").to_string(), )) .child( h_flex().w_full().justify_end().child( Button::new("run-generate") .small() .primary() - .label("生成运行配置") + .label( + crate::i18n::menu_text(cx, "settings.run.generate").to_string(), + ) .on_click(cx.listener(|_this, _event, _window, cx| { // 运行配置生成尚未接入后端。 cx.notify(); @@ -1004,17 +1101,23 @@ impl SettingsDialog { /// 编辑器:对齐 Tauri `EditorPanel`(显示/编辑器标签页/缩进)。 fn render_editor_content(&self, cx: &mut Context) -> impl IntoElement { let s = settings::get(cx).clone(); + let spaces = crate::i18n::menu_text(cx, "settings.mac.spaces").to_string(); + let tab_options = [ + ("2", format!("2 {spaces}")), + ("4", format!("4 {spaces}")), + ("8", format!("8 {spaces}")), + ]; v_flex() .w_full() .gap_4() .child( self.render_group( - "显示", + crate::i18n::menu_text(cx, "settings.mac.display").to_string(), v_flex() .w_full() .gap_3() .child(self.render_row( - "字体大小".to_string(), + crate::i18n::menu_text(cx, "settings.mac.fontSize").to_string(), None, self.render_stepper( "font-dec", @@ -1030,7 +1133,7 @@ impl SettingsDialog { ), )) .child(self.render_row( - "显示用法与 Git 作者".to_string(), + crate::i18n::menu_text(cx, "settings.mac.showCodeVision").to_string(), None, self.render_toggle("editor-code-lens", s.code_lens, cx, |this, cx| { this.commit(cx, |s| s.code_lens = !s.code_lens) @@ -1038,31 +1141,49 @@ impl SettingsDialog { )), ), ) - .child(self.render_group( - "编辑器标签页", - v_flex().w_full().gap_3().child(self.render_row( - "缓冲区轮播".to_string(), - Some("在主视图中将打开的缓冲区显示为可横向滚动的轮播".to_string()), - self.render_toggle( - "editor-buffer-carousel", - s.horizontal_tab_scroll, - cx, - |this, cx| { - this.commit(cx, |s| s.horizontal_tab_scroll = !s.horizontal_tab_scroll) - }, + .child( + self.render_group( + crate::i18n::menu_text(cx, "settings.mac.editorTabs").to_string(), + v_flex().w_full().gap_3().child( + self.render_row( + crate::i18n::menu_text(cx, "settings.editor.bufferCarousel") + .to_string() + .to_string(), + Some( + crate::i18n::menu_text( + cx, + "settings.editor.bufferCarouselDescription", + ) + .to_string(), + ), + self.render_toggle( + "editor-buffer-carousel", + s.horizontal_tab_scroll, + cx, + |this, cx| { + this.commit(cx, |s| { + s.horizontal_tab_scroll = !s.horizontal_tab_scroll + }) + }, + ), + ), ), - )), - )) + ), + ) .child(self.render_group( - "缩进", + crate::i18n::menu_text(cx, "settings.mac.indentation").to_string(), v_flex().w_full().gap_3().child(self.render_row( - "制表符宽度".to_string(), + crate::i18n::menu_text(cx, "settings.mac.tabWidth").to_string(), None, self.render_dropdown( "editor-tab-width", s.tab_size.to_string(), 128.0, - &[("2", "2 个空格"), ("4", "4 个空格"), ("8", "8 个空格")], + vec![ + ("2", tab_options[0].1.clone()), + ("4", tab_options[1].1.clone()), + ("8", tab_options[2].1.clone()), + ], cx, |this, size, cx| { this.commit(cx, |s| s.tab_size = size.parse().unwrap_or(2)) @@ -1079,19 +1200,19 @@ impl SettingsDialog { .w_full() .gap_4() .child(self.render_group( - "快捷键方案", + crate::i18n::menu_text(cx, "settings.mac.keymapPreset").to_string(), v_flex().w_full().gap_3().child(self.render_row( - "预设".to_string(), + crate::i18n::menu_text(cx, "settings.mac.preset").to_string(), None, self.render_dropdown( "keymap-preset", s.keybinding_preset.clone(), 176.0, - &[ - ("none", "Lithe"), - ("vscode", "Visual Studio Code"), - ("jetbrains", "JetBrains"), - ("xcode", "Xcode"), + vec![ + ("none", "Lithe".to_string()), + ("vscode", "Visual Studio Code".to_string()), + ("jetbrains", "JetBrains".to_string()), + ("xcode", "Xcode".to_string()), ], cx, |this, preset, cx| { @@ -1102,7 +1223,7 @@ impl SettingsDialog { )) .child( self.render_group( - "键盘快捷键", + crate::i18n::menu_text(cx, "settings.mac.shortcuts").to_string(), v_flex() .w_full() .gap_3() @@ -1128,12 +1249,16 @@ impl SettingsDialog { .flex_1() .text_xs() .text_color(ThemeColors::subtle_foreground()) - .child("搜索快捷键"), + .child(crate::i18n::menu_text( + cx, + "settings.mac.searchShortcuts", + )), ), ) .child( self.render_note( - "选择快捷键预设,然后使用命令面板查看和运行可用命令。", + crate::i18n::menu_text(cx, "settings.mac.shortcutsDescription") + .to_string(), ), ), ), @@ -1144,20 +1269,37 @@ impl SettingsDialog { fn render_terminal_content(&self, cx: &mut Context) -> impl IntoElement { let s = settings::get(cx).clone(); v_flex().w_full().gap_4().child(self.render_group( - "Shell", + crate::i18n::menu_text(cx, "settings.mac.shell").to_string(), v_flex().w_full().gap_3().child(self.render_row( - "默认 Shell".to_string(), - Some("用于新的终端会话。".to_string()), + crate::i18n::menu_text(cx, "settings.mac.defaultShell").to_string(), + Some( + crate::i18n::menu_text(cx, "settings.mac.defaultShellDescription").to_string(), + ), self.render_dropdown( "terminal-default-shell", s.terminal_default_shell_id.clone(), 176.0, - &[ - ("", "系统默认"), - ("powershell", "PowerShell"), - ("cmd", "命令提示符"), - ("wsl", "WSL"), - ], + vec![ + ( + "", + crate::i18n::menu_text(cx, "settings.mac.systemDefault") + .to_string(), + ), + ( + "powershell", + crate::i18n::menu_text(cx, "settings.mac.shellPowerShell") + .to_string(), + ), + ( + "cmd", + crate::i18n::menu_text(cx, "settings.mac.shellCommandPrompt") + .to_string(), + ), + ( + "wsl", + crate::i18n::menu_text(cx, "settings.mac.shellWsl").to_string(), + ), + ], cx, |this, shell, cx| { this.commit(cx, |s| s.terminal_default_shell_id = shell.to_string()) @@ -1175,24 +1317,34 @@ impl SettingsDialog { .gap_4() .child( self.render_group( - "语言服务", + crate::i18n::menu_text(cx, "settings.mac.languageServices").to_string(), v_flex() .w_full() .gap_3() - .child(self.render_row( - "自动补全".to_string(), - Some("显示活动语言服务器提供的补全建议。".to_string()), - self.render_toggle( - "lsp-auto-completion", - s.auto_completion, - cx, - |this, cx| { - this.commit(cx, |s| s.auto_completion = !s.auto_completion) - }, + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.mac.autoCompletion") + .to_string() + .to_string(), + Some( + crate::i18n::menu_text( + cx, + "settings.mac.autoCompletionDescription", + ) + .to_string(), + ), + self.render_toggle( + "lsp-auto-completion", + s.auto_completion, + cx, + |this, cx| { + this.commit(cx, |s| s.auto_completion = !s.auto_completion) + }, + ), ), - )) + ) .child(self.render_row( - "参数提示".to_string(), + crate::i18n::menu_text(cx, "settings.mac.parameterHints").to_string(), None, self.render_toggle( "lsp-parameter-hints", @@ -1203,83 +1355,122 @@ impl SettingsDialog { }, ), )) - .child(self.render_row( - "语义高亮".to_string(), - None, - self.render_toggle( - "lsp-semantic-highlighting", - s.semantic_tokens, - cx, - |this, cx| { - this.commit(cx, |s| s.semantic_tokens = !s.semantic_tokens) - }, + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.mac.semanticHighlighting") + .to_string() + .to_string(), + None, + self.render_toggle( + "lsp-semantic-highlighting", + s.semantic_tokens, + cx, + |this, cx| { + this.commit(cx, |s| s.semantic_tokens = !s.semantic_tokens) + }, + ), ), - )), + ), + ), + ) + .child( + self.render_group( + crate::i18n::menu_text(cx, "settings.mac.detectedServers").to_string(), + self.render_note( + crate::i18n::menu_text(cx, "settings.mac.detectedServersDescription") + .to_string(), + ), ), ) - .child(self.render_group( - "已检测语言服务器", - self.render_note("语言服务器由已安装的语言扩展检测,并在打开受支持文件时启动。"), - )) } /// AI 聊天与编辑:对齐 `AISettings` 的 Lithe Agent 分组结构 /// (提供商/模型行),完整选择器尚未接入后端。 - fn render_ai_content(&self, _cx: &mut Context) -> impl IntoElement { + fn render_ai_content(&self, cx: &mut Context) -> impl IntoElement { v_flex() .w_full() .gap_4() .child( self.render_group( - "Lithe Agent", + "Lithe Agent".to_string(), v_flex() .w_full() .gap_3() - .child(self.render_row( - "提供商".to_string(), - Some("选择 Lithe Agent 使用的提供商".to_string()), - self.render_value("Anthropic".to_string()), - )) - .child(self.render_row( - "模型".to_string(), - Some("选择 Lithe Agent 使用的模型".to_string()), - self.render_value("claude-sonnet-4-6".to_string()), - )), + .child( + self.render_row( + crate::i18n::menu_text(cx, "aiSettings.provider").to_string(), + Some( + crate::i18n::menu_text(cx, "aiSettings.providerDescription") + .to_string() + .to_string(), + ), + self.render_value("Anthropic".to_string()), + ), + ) + .child( + self.render_row( + crate::i18n::menu_text(cx, "aiSettings.model").to_string(), + Some( + crate::i18n::menu_text(cx, "aiSettings.modelDescription") + .to_string() + .to_string(), + ), + self.render_value("claude-sonnet-4-6".to_string()), + ), + ), ), ) .child(self.render_group( - "说明", - self.render_note("完整提供商与模型选择尚未接入,后续版本提供。"), + crate::i18n::menu_text(cx, "settings.ai.noteTitle").to_string(), + self.render_note( + crate::i18n::menu_text(cx, "settings.ai.providerNote").to_string(), + ), )) } /// AI 与提交:对齐 `AiCommitSettingsPanel` 的分组结构 /// (配置文件/规则),完整配置尚未接入后端。 - fn render_ai_commit_content(&self, _cx: &mut Context) -> impl IntoElement { + fn render_ai_commit_content(&self, cx: &mut Context) -> impl IntoElement { v_flex() .w_full() .gap_4() .child( self.render_group( - "提交信息", + crate::i18n::menu_text(cx, "settings.mac.commitMessage").to_string(), v_flex() .w_full() .gap_3() - .child(self.render_row( - "配置文件".to_string(), - Some("用于生成提交信息的模型配置".to_string()), - self.render_value("默认".to_string()), - )) - .child(self.render_row( - "规则".to_string(), - Some("生成提交信息时遵循的规则".to_string()), - self.render_value("默认".to_string()), - )), + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.ai.commitProfile").to_string(), + Some( + crate::i18n::menu_text( + cx, + "settings.ai.commitProfileDescription", + ) + .to_string(), + ), + self.render_value("默认".to_string()), + ), + ) + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.ai.commitRules").to_string(), + Some( + crate::i18n::menu_text( + cx, + "settings.ai.commitRulesDescription", + ) + .to_string(), + ), + self.render_value("默认".to_string()), + ), + ), ), ) .child(self.render_group( - "说明", - self.render_note("AI 提交配置尚未接入,后续版本提供。"), + crate::i18n::menu_text(cx, "settings.ai.noteTitle").to_string(), + self.render_note(crate::i18n::menu_text(cx, "settings.ai.commitNote").to_string()), )) } @@ -1292,13 +1483,13 @@ impl SettingsDialog { .gap_4() .child( self.render_group( - "Fetch 默认行为", + crate::i18n::menu_text(cx, "git.fetch.defaults").to_string(), v_flex() .w_full() .gap_3() .child(self.render_row( - "清理失效的远程跟踪引用".to_string(), - Some("用于所有项目中的普通 Fetch。".to_string()), + crate::i18n::menu_text(cx, "git.fetch.prune").to_string(), + Some(crate::i18n::menu_text(cx, "git.fetch.scope").to_string()), self.render_toggle( "git-fetch-prune", s.git_fetch_prune, @@ -1309,18 +1500,40 @@ impl SettingsDialog { ), )) .child(self.render_row( - "获取子模块".to_string(), + crate::i18n::menu_text(cx, "git.fetch.submodules").to_string(), None, self.render_dropdown( "git-fetch-submodules", s.git_fetch_submodules.clone(), 160.0, - &[ - ("inherit", "使用 Git 配置"), - ("no", "不获取子模块"), - ("onDemand", "按需获取"), - ("yes", "获取全部子模块"), - ], + vec![ + ( + "inherit", + crate::i18n::menu_text( + cx, + "git.fetch.submodules.inherit", + ) + .to_string(), + ), + ( + "no", + crate::i18n::menu_text(cx, "git.fetch.submodules.no") + .to_string(), + ), + ( + "onDemand", + crate::i18n::menu_text( + cx, + "git.fetch.submodules.onDemand", + ) + .to_string(), + ), + ( + "yes", + crate::i18n::menu_text(cx, "git.fetch.submodules.yes") + .to_string(), + ), + ], cx, |this, value, cx| { this.commit(cx, |s| s.git_fetch_submodules = value.to_string()) @@ -1328,18 +1541,34 @@ impl SettingsDialog { ), )) .child(self.render_row( - "获取标签".to_string(), - Some("凭据沿用现有 Git 凭据助手和 SSH 配置。".to_string()), + crate::i18n::menu_text(cx, "git.fetch.tags").to_string(), + Some(crate::i18n::menu_text(cx, "git.fetch.credentials").to_string()), self.render_dropdown( "git-fetch-tags", s.git_fetch_tags.clone(), 160.0, - &[ - ("inherit", "使用 Git 配置"), - ("all", "获取全部标签"), - ("none", "不获取标签"), - ("prune", "同步标签并删除远程已不存在的本地标签"), - ], + vec![ + ( + "inherit", + crate::i18n::menu_text(cx, "git.fetch.tags.inherit") + .to_string(), + ), + ( + "all", + crate::i18n::menu_text(cx, "git.fetch.tags.all") + .to_string(), + ), + ( + "none", + crate::i18n::menu_text(cx, "git.fetch.tags.none") + .to_string(), + ), + ( + "prune", + crate::i18n::menu_text(cx, "git.fetch.tags.prune") + .to_string(), + ), + ], cx, |this, value, cx| { this.commit(cx, |s| s.git_fetch_tags = value.to_string()) @@ -1350,192 +1579,312 @@ impl SettingsDialog { ) .child( self.render_group( - "集成", + crate::i18n::menu_text(cx, "settings.git.integration").to_string(), v_flex() .w_full() .gap_3() - .child(self.render_row( - "Git 集成".to_string(), - Some("启用 Git 仓库的源代码管理功能".to_string()), - self.render_toggle( - "git-integration", - s.core_features.git, - cx, - |this, cx| { - this.commit(cx, |s| s.core_features.git = !s.core_features.git) - }, + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.git.gitIntegration") + .to_string() + .to_string(), + Some( + crate::i18n::menu_text( + cx, + "settings.git.gitIntegrationDescription", + ) + .to_string(), + ), + self.render_toggle( + "git-integration", + s.core_features.git, + cx, + |this, cx| { + this.commit(cx, |s| { + s.core_features.git = !s.core_features.git + }) + }, + ), ), - )) - .child(self.render_row( - "自动刷新 Git 状态".to_string(), - Some("相关文件或 Git 事件发生变化后自动刷新 Git 视图".to_string()), - self.render_toggle( - "git-auto-refresh", - s.auto_refresh_git_status, - cx, - |this, cx| { - this.commit(cx, |s| { - s.auto_refresh_git_status = !s.auto_refresh_git_status - }) - }, + ) + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.git.autoRefresh").to_string(), + Some( + crate::i18n::menu_text( + cx, + "settings.git.autoRefreshDescription", + ) + .to_string(), + ), + self.render_toggle( + "git-auto-refresh", + s.auto_refresh_git_status, + cx, + |this, cx| { + this.commit(cx, |s| { + s.auto_refresh_git_status = !s.auto_refresh_git_status + }) + }, + ), ), - )) - .child(self.render_row( - "丢弃前确认".to_string(), - Some("丢弃文件或仓库更改前显示确认提示".to_string()), - self.render_toggle( - "git-confirm-discard", - s.confirm_before_discard, - cx, - |this, cx| { - this.commit(cx, |s| { - s.confirm_before_discard = !s.confirm_before_discard - }) - }, + ) + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.git.confirmDiscard") + .to_string() + .to_string(), + Some( + crate::i18n::menu_text( + cx, + "settings.git.confirmDiscardDescription", + ) + .to_string(), + ), + self.render_toggle( + "git-confirm-discard", + s.confirm_before_discard, + cx, + |this, cx| { + this.commit(cx, |s| { + s.confirm_before_discard = !s.confirm_before_discard + }) + }, + ), ), - )), + ), ), ) .child( self.render_group( - "Git 视图", + crate::i18n::menu_text(cx, "settings.git.view").to_string(), v_flex() .w_full() .gap_3() - .child(self.render_row( - "基于文件夹的更改".to_string(), - Some("以类似文件视图的文件夹树形式显示 Git 更改".to_string()), - self.render_toggle( - "git-folder-changes", - s.git_changes_folder_view, - cx, - |this, cx| { - this.commit(cx, |s| { - s.git_changes_folder_view = !s.git_changes_folder_view - }) - }, + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.git.folderChanges") + .to_string() + .to_string(), + Some( + crate::i18n::menu_text( + cx, + "settings.git.folderChangesDescription", + ) + .to_string(), + ), + self.render_toggle( + "git-folder-changes", + s.git_changes_folder_view, + cx, + |this, cx| { + this.commit(cx, |s| { + s.git_changes_folder_view = !s.git_changes_folder_view + }) + }, + ), ), - )) - .child(self.render_row( - "显示未跟踪文件".to_string(), - Some("在 Git 状态面板中显示未跟踪文件".to_string()), - self.render_toggle( - "git-show-untracked", - s.show_untracked_files, - cx, - |this, cx| { - this.commit(cx, |s| { - s.show_untracked_files = !s.show_untracked_files - }) - }, + ) + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.git.untracked").to_string(), + Some( + crate::i18n::menu_text(cx, "settings.git.untrackedDescription") + .to_string() + .to_string(), + ), + self.render_toggle( + "git-show-untracked", + s.show_untracked_files, + cx, + |this, cx| { + this.commit(cx, |s| { + s.show_untracked_files = !s.show_untracked_files + }) + }, + ), ), - )) - .child(self.render_row( - "优先显示已暂存项".to_string(), - Some("在 Git 面板中将已暂存更改显示在未暂存更改之前".to_string()), - self.render_toggle( - "git-show-staged-first", - s.show_staged_first, - cx, - |this, cx| { - this.commit(cx, |s| s.show_staged_first = !s.show_staged_first) - }, + ) + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.git.stagedFirst").to_string(), + Some( + crate::i18n::menu_text( + cx, + "settings.git.stagedFirstDescription", + ) + .to_string(), + ), + self.render_toggle( + "git-show-staged-first", + s.show_staged_first, + cx, + |this, cx| { + this.commit(cx, |s| { + s.show_staged_first = !s.show_staged_first + }) + }, + ), ), - )) - .child(self.render_row( - "单击打开差异".to_string(), - Some("单击已更改文件时打开差异,而不是直接打开文件".to_string()), - self.render_toggle( - "git-open-diff-on-click", - s.open_diff_on_click, - cx, - |this, cx| { - this.commit(cx, |s| { - s.open_diff_on_click = !s.open_diff_on_click - }) - }, + ) + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.git.openDiff").to_string(), + Some( + crate::i18n::menu_text(cx, "settings.git.openDiffDescription") + .to_string() + .to_string(), + ), + self.render_toggle( + "git-open-diff-on-click", + s.open_diff_on_click, + cx, + |this, cx| { + this.commit(cx, |s| { + s.open_diff_on_click = !s.open_diff_on_click + }) + }, + ), ), - )) - .child(self.render_row( - "紧凑 Git 状态标记".to_string(), - Some("在 Git 面板中使用更紧凑的差异统计和暂存标签布局".to_string()), - self.render_toggle( - "git-compact-badges", - s.compact_git_status_badges, - cx, - |this, cx| { - this.commit(cx, |s| { - s.compact_git_status_badges = !s.compact_git_status_badges - }) - }, + ) + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.git.compactBadges") + .to_string() + .to_string(), + Some( + crate::i18n::menu_text( + cx, + "settings.git.compactBadgesDescription", + ) + .to_string(), + ), + self.render_toggle( + "git-compact-badges", + s.compact_git_status_badges, + cx, + |this, cx| { + this.commit(cx, |s| { + s.compact_git_status_badges = + !s.compact_git_status_badges + }) + }, + ), ), - )) - .child(self.render_row( - "折叠空分区".to_string(), - Some("没有项目时隐藏已暂存更改等空 Git 分区".to_string()), - self.render_toggle( - "git-collapse-empty", - s.collapse_empty_git_sections, + ) + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.git.collapseEmpty") + .to_string() + .to_string(), + Some( + crate::i18n::menu_text( + cx, + "settings.git.collapseEmptyDescription", + ) + .to_string(), + ), + self.render_toggle( + "git-collapse-empty", + s.collapse_empty_git_sections, + cx, + |this, cx| { + this.commit(cx, |s| { + s.collapse_empty_git_sections = + !s.collapse_empty_git_sections + }) + }, + ), + ), + ) + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.git.rememberPanel") + .to_string() + .to_string(), + Some( + crate::i18n::menu_text( + cx, + "settings.git.rememberPanelDescription", + ) + .to_string(), + ), + self.render_toggle( + "git-remember-panel", + s.remember_last_git_panel_mode, + cx, + |this, cx| { + this.commit(cx, |s| { + s.remember_last_git_panel_mode = + !s.remember_last_git_panel_mode + }) + }, + ), + ), + ), + ), + ) + .child( + self.render_group( + crate::i18n::menu_text(cx, "settings.git.defaultDiff").to_string(), + v_flex().w_full().gap_3().child( + self.render_row( + crate::i18n::menu_text(cx, "settings.git.defaultDiff").to_string(), + Some( + crate::i18n::menu_text(cx, "settings.git.defaultDiffDescription") + .to_string() + .to_string(), + ), + self.render_dropdown( + "git-default-diff-view", + s.git_default_diff_view.clone(), + 160.0, + vec![ + ( + "unified", + crate::i18n::menu_text(cx, "settings.git.unified") + .to_string(), + ), + ( + "split", + crate::i18n::menu_text(cx, "settings.git.split") + .to_string(), + ), + ], cx, - |this, cx| { - this.commit(cx, |s| { - s.collapse_empty_git_sections = - !s.collapse_empty_git_sections - }) + |this, value, cx| { + this.commit(cx, |s| s.git_default_diff_view = value.to_string()) }, ), - )) - .child(self.render_row( - "记住上次 Git 面板模式".to_string(), - Some("重新打开 Git 视图时恢复上次打开的底部 Git 面板分区".to_string()), + ), + ), + ), + ) + .child( + self.render_group( + crate::i18n::menu_text(cx, "settings.git.editor").to_string(), + v_flex().w_full().gap_3().child( + self.render_row( + crate::i18n::menu_text(cx, "settings.git.inlineBlame").to_string(), + Some( + crate::i18n::menu_text(cx, "settings.git.inlineBlameDescription") + .to_string() + .to_string(), + ), self.render_toggle( - "git-remember-panel", - s.remember_last_git_panel_mode, + "git-inline-blame", + s.enable_inline_git_blame, cx, |this, cx| { this.commit(cx, |s| { - s.remember_last_git_panel_mode = - !s.remember_last_git_panel_mode + s.enable_inline_git_blame = !s.enable_inline_git_blame }) }, ), - )), + ), + ), ), ) - .child(self.render_group( - "默认差异视图", - v_flex().w_full().gap_3().child(self.render_row( - "默认差异视图".to_string(), - Some("选择 Git 差异的默认布局".to_string()), - self.render_dropdown( - "git-default-diff-view", - s.git_default_diff_view.clone(), - 160.0, - &[("unified", "统一视图"), ("split", "拆分视图")], - cx, - |this, value, cx| { - this.commit(cx, |s| s.git_default_diff_view = value.to_string()) - }, - ), - )), - )) - .child(self.render_group( - "编辑器", - v_flex().w_full().gap_3().child(self.render_row( - "启用行内 Blame".to_string(), - Some("在编辑器中显示当前行的 Git Blame 元数据".to_string()), - self.render_toggle( - "git-inline-blame", - s.enable_inline_git_blame, - cx, - |this, cx| { - this.commit(cx, |s| { - s.enable_inline_git_blame = !s.enable_inline_git_blame - }) - }, - ), - )), - )) } /// 日志:对齐 Tauri `LogSettingsPanel` 的分组结构 @@ -1547,95 +1896,150 @@ impl SettingsDialog { .gap_4() .child( self.render_group( - "日志位置", + crate::i18n::menu_text(cx, "settings.logs.locations").to_string(), v_flex() .w_full() .gap_3() - .child(self.render_row( - "当前日志位置".to_string(), - Some("本次会话实际写入日志的目录。".to_string()), - self.render_value(default_dir.clone()), - )) - .child(self.render_row( - "默认日志位置".to_string(), - Some("Lithe 始终维护并清理这个应用自有目录。".to_string()), - self.render_value(default_dir), - )) - .child(self.render_row( - "自定义日志位置".to_string(), - Some("选择父目录后,Lithe 会写入其中的 Lithe/logs 子目录。".to_string()), - h_flex().gap_1p5().child( - Button::new("logs-choose-dir") - .small() - .ghost() - .label("选择…") - .on_click(cx.listener(|_this, _event, _window, cx| { - // 目录选择尚未接入后端。 - cx.notify(); - })), + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.logs.effectivePath") + .to_string() + .to_string(), + Some( + crate::i18n::menu_text( + cx, + "settings.logs.effectivePathDescription", + ) + .to_string(), + ), + self.render_value(default_dir.clone()), ), - )), + ) + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.logs.defaultPath").to_string(), + Some( + crate::i18n::menu_text( + cx, + "settings.logs.defaultPathDescription", + ) + .to_string(), + ), + self.render_value(default_dir), + ), + ) + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.logs.customPath").to_string(), + Some( + crate::i18n::menu_text( + cx, + "settings.logs.customPathDescription", + ) + .to_string(), + ), + h_flex().gap_1p5().child( + Button::new("logs-choose-dir") + .small() + .ghost() + .label( + crate::i18n::menu_text(cx, "settings.logs.choose") + .to_string(), + ) + .on_click(cx.listener(|_this, _event, _window, cx| { + // 目录选择尚未接入后端。 + cx.notify(); + })), + ), + ), + ), ), ) .child( self.render_group( - "诊断", - v_flex().w_full().gap_3().child(self.render_row( - "本次会话启用诊断日志".to_string(), - Some( - "记录 DEBUG 事件和低频 FPS 心跳;Lithe 重启后自动恢复为 INFO。" + crate::i18n::menu_text(cx, "settings.logs.diagnostics").to_string(), + v_flex().w_full().gap_3().child( + self.render_row( + crate::i18n::menu_text(cx, "settings.logs.diagnosticMode").to_string(), + Some( + crate::i18n::menu_text( + cx, + "settings.logs.diagnosticModeDescription", + ) .to_string(), + ), + self.render_toggle( + "logs-diagnostic-mode", + self.diagnostic_mode, + cx, + |this, cx| { + this.diagnostic_mode = !this.diagnostic_mode; + cx.notify(); + }, + ), ), - self.render_toggle( - "logs-diagnostic-mode", - self.diagnostic_mode, - cx, - |this, cx| { - this.diagnostic_mode = !this.diagnostic_mode; - cx.notify(); - }, - ), - )), + ), ), ) .child( self.render_group( - "保留策略", + crate::i18n::menu_text(cx, "settings.logs.retention").to_string(), v_flex() .w_full() .gap_3() - .child(self.render_note( - "单个日志达到 10 MB 后轮转,每天保留最新五个常规日志,并在 Lithe 启动时删除超过 30 天的日志。", - )) - .child(self.render_row( - "清除当前目录日志".to_string(), - Some("只删除当前目录中由 Lithe 管理的日志文件。".to_string()), - Button::new("logs-clear") + .child( + self.render_note( + crate::i18n::menu_text(cx, "settings.logs.retentionDescription") + .to_string(), + ), + ) + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.logs.clearCurrent") + .to_string() + .to_string(), + Some( + crate::i18n::menu_text( + cx, + "settings.logs.clearCurrentDescription", + ) + .to_string(), + ), + Button::new("logs-clear") + .small() + .ghost() + .label( + crate::i18n::menu_text(cx, "settings.logs.clearLogs") + .to_string(), + ) + .on_click(cx.listener(|_this, _event, _window, cx| { + // 日志清理尚未接入后端。 + cx.notify(); + })), + ), + ), + ), + ) + .child( + self.render_group( + crate::i18n::menu_text(cx, "settings.logs.diagnosticBundle").to_string(), + v_flex().w_full().gap_3().child( + self.render_row( + crate::i18n::menu_text(cx, "settings.logs.exportBundle").to_string(), + None, + Button::new("logs-export-bundle") .small() .ghost() - .label("清除日志") + .label(crate::i18n::menu_text( + cx, + "settings.logs.exportBundleConfirm", + )) .on_click(cx.listener(|_this, _event, _window, cx| { - // 日志清理尚未接入后端。 + // 诊断包导出尚未接入后端。 cx.notify(); })), - )), - ), - ) - .child( - self.render_group( - "诊断包", - v_flex().w_full().gap_3().child(self.render_row( - "导出诊断包…".to_string(), - None, - Button::new("logs-export-bundle") - .small() - .ghost() - .label("导出") - .on_click(cx.listener(|_this, _event, _window, cx| { - // 诊断包导出尚未接入后端。 - cx.notify(); - })), - )), + ), + ), ), ) } @@ -1644,25 +2048,34 @@ impl SettingsDialog { fn render_updates_content(&self, cx: &mut Context) -> impl IntoElement { v_flex().w_full().gap_4().child( self.render_group( - "软件更新", + crate::i18n::menu_text(cx, "settings.mac.softwareUpdate").to_string(), v_flex() .w_full() .gap_3() .child( self.render_row( "Lithe".to_string(), - Some(format!("当前版本:{}", env!("CARGO_PKG_VERSION"))), + Some( + crate::i18n::menu_text(cx, "settings.mac.currentVersion") + .to_string() + .replace("{version}", env!("CARGO_PKG_VERSION")), + ), Button::new("check-updates") .small() .primary() - .label("检查更新") + .label( + crate::i18n::menu_text(cx, "settings.mac.checkForUpdates") + .to_string(), + ) .on_click(cx.listener(|_this, _event, _window, cx| { // 更新检查尚未接入后端。 cx.notify(); })), ), ) - .child(self.render_note("Lithe 可以检查新的预览版和稳定版。")), + .child(self.render_note( + crate::i18n::menu_text(cx, "settings.mac.updateHint").to_string(), + )), ), ) } diff --git a/linux/src/workbench/sidebar.rs b/linux/src/workbench/sidebar.rs index 4de434712..630bfa696 100644 --- a/linux/src/workbench/sidebar.rs +++ b/linux/src/workbench/sidebar.rs @@ -401,8 +401,8 @@ impl Render for SidebarView { // 标题跟随语言:Explorer 用 workbench.project(项目/Project)。 let title = match self.active_tab { SidebarTab::Explorer => crate::i18n::menu_text(cx, "workbench.project").to_string(), - SidebarTab::Search => "SEARCH".to_string(), - SidebarTab::Git => "GIT".to_string(), + SidebarTab::Search => crate::i18n::menu_text(cx, "workbench.search").to_string(), + SidebarTab::Git => crate::i18n::menu_text(cx, "workbench.sourceControl").to_string(), }; // 过滤行在链式构建前算好,避免在 `.when` 闭包里同时借用 self 与 cx。 let filter_row = if self.show_tree_filter { @@ -506,7 +506,7 @@ impl Render for SidebarView { .small() .ghost() .icon(IconName::RotateCw) - .tooltip("Refresh") + .tooltip(crate::i18n::menu_text(cx, "ui.refresh")) .on_click(cx.listener(|this, _event, _window, cx| { this.refresh(cx); this.refresh_git(cx); @@ -847,7 +847,7 @@ impl SidebarView { .text_xs() .text_color(ThemeColors::text_muted()) .p_2() - .child("Loading workspace files..."), + .child(format!("{}...", crate::i18n::menu_text(cx, "ui.loading"))), ) }) .when_some(self.error_message.as_ref(), |this, err| { @@ -973,10 +973,9 @@ impl SidebarView { .text_color(ThemeColors::text_muted()), ) .child( - div() - .text_xs() - .text_color(ThemeColors::text_muted()) - .child("Search files or text..."), + div().text_xs().text_color(ThemeColors::text_muted()).child( + crate::i18n::menu_text(cx, "workbench.searchInFiles").to_string(), + ), ), ) .when(self.is_searching, |this| { @@ -985,7 +984,7 @@ impl SidebarView { .text_xs() .text_color(ThemeColors::text_muted()) .p_1() - .child("Searching..."), + .child(format!("{}...", crate::i18n::menu_text(cx, "ui.loading"))), ) }) .children(self.search_results.iter().enumerate().map(|(idx, res)| { @@ -1074,7 +1073,11 @@ impl SidebarView { div() .text_xs() .text_color(ThemeColors::text_muted()) - .child(format!("Changed Files ({})", self.git_changes.len())), + .child(format!( + "{} ({})", + crate::i18n::menu_text(cx, "workbench.changes"), + self.git_changes.len() + )), ) .children(self.git_changes.iter().enumerate().map(|(idx, change)| { let path = change.path.clone(); @@ -1160,7 +1163,8 @@ impl SidebarView { ThemeColors::text_primary() }) .child(if self.git_commit_message.is_empty() { - "Commit message (e.g. feat: update files)...".to_string() + crate::i18n::menu_text(cx, "git.commitMessagePlaceholder") + .to_string() } else { self.git_commit_message.clone() }), @@ -1170,18 +1174,19 @@ impl SidebarView { .w_full() .items_center() .justify_between() - .child( - div() - .text_xs() - .text_color(ThemeColors::text_muted()) - .child(format!("{} changed", self.git_changes.len())), - ) + .child(div().text_xs().text_color(ThemeColors::text_muted()).child( + format!( + "{} ({})", + crate::i18n::menu_text(cx, "workbench.changes"), + self.git_changes.len() + ), + )) .child( Button::new("git-commit-btn") .small() .primary() .icon(IconName::Check) - .label("Commit") + .label(crate::i18n::menu_text(cx, "git.commit")) .on_click(cx.listener(|this, _event, _window, cx| { let msg = if this.git_commit_message.trim().is_empty() { "feat: update workspace files".to_string() diff --git a/linux/src/workbench/status_bar.rs b/linux/src/workbench/status_bar.rs index 46cc6a94d..d75e0b3c2 100644 --- a/linux/src/workbench/status_bar.rs +++ b/linux/src/workbench/status_bar.rs @@ -126,15 +126,10 @@ impl StatusBarView { fn render_leading_item(&self, id: &str) -> Option { match id { "filePath" => { - let label = self - .current_file - .clone() - .unwrap_or_else(|| "No file active".to_string()); - Some(labeled( - IconName::FileText, - label, - ThemeColors::subtle_foreground(), - )) + // 无活动文件时整项不渲染,对齐 Tauri(filePath 为 null)。 + self.current_file.clone().map(|label| { + labeled(IconName::FileText, label, ThemeColors::subtle_foreground()) + }) } // 分支未知时整项不渲染,避免用占位名误导用户。 "branch" => self diff --git a/linux/src/workbench/terminal.rs b/linux/src/workbench/terminal.rs index bb3dca67c..2bcc79919 100644 --- a/linux/src/workbench/terminal.rs +++ b/linux/src/workbench/terminal.rs @@ -170,7 +170,7 @@ impl Render for TerminalView { Button::new("clear-term") .small() .ghost() - .label("Clear") + .label(crate::i18n::menu_text(cx, "ui.clear")) .on_click(cx.listener(|this, _event, _window, cx| { this.clear(cx); })), diff --git a/linux/src/workbench/toolbar.rs b/linux/src/workbench/toolbar.rs index 0a642d223..12e805ad5 100644 --- a/linux/src/workbench/toolbar.rs +++ b/linux/src/workbench/toolbar.rs @@ -509,7 +509,6 @@ impl Render for ToolbarView { .small() .ghost() .icon(IconName::List) - .tooltip("Menu") .selected(open) .on_click(cx.listener( |this, _event: &gpui_kit::ClickEvent, _window, cx| { @@ -694,7 +693,7 @@ impl Render for ToolbarView { .small() .ghost() .icon(IconName::Search) - .tooltip("Search") + .tooltip(crate::i18n::menu_text(cx, "menu.quickOpen")) .on_click(cx.listener(|_this, _event, _window, cx| { cx.emit(ToolbarEvent::QuickOpen); })), diff --git a/linux/src/workbench/welcome_screen.rs b/linux/src/workbench/welcome_screen.rs index 1c886080d..07704ce11 100644 --- a/linux/src/workbench/welcome_screen.rs +++ b/linux/src/workbench/welcome_screen.rs @@ -276,7 +276,8 @@ impl WelcomeScreenView { ThemeColors::foreground() }) .child(if self.query.is_empty() { - "Search projects".to_string() + crate::i18n::menu_text(cx, "welcome.searchProjects") + .to_string() } else { self.query.clone() }), @@ -328,7 +329,10 @@ impl WelcomeScreenView { .text_sm() .font_weight(FontWeight::MEDIUM) .text_color(ThemeColors::foreground()) - .child("No recent projects"), + .child(crate::i18n::menu_text( + cx, + "welcome.noRecentProjects", + )), ) .child( div() @@ -402,7 +406,16 @@ impl WelcomeScreenView { .xsmall() .ghost() .icon(IconName::Trash) - .tooltip("Remove from recent") + .tooltip( + crate::i18n::menu_text(cx, "welcome.removeRecent") + .replace( + "{name}", + remove_path + .rsplit('/') + .next() + .unwrap_or(&remove_path), + ), + ) .on_click(cx.listener( move |this, _event, _window, cx| { // 阻止冒泡到整行,避免删除时同时打开项目 From 183d4223547f5ff92556c5a955a47af901291f93 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 17:36:27 +0800 Subject: [PATCH 18/59] =?UTF-8?q?feat(linux):=20=E6=96=B0=E5=BB=BA/?= =?UTF-8?q?=E6=89=93=E5=BC=80/=E5=85=8B=E9=9A=86=E4=BB=93=E5=BA=93?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E6=8E=A5=E5=85=A5=EF=BC=8C=E5=AF=B9=E9=BD=90?= =?UTF-8?q?=20Tauri?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对照 new-project-content.tsx / new-project-model.ts / project-picker.tsx: - 新增 ProjectDialog(遮罩+560x480 卡片):New 模式来源列表 (空项目/Next.js/Vite+React,搜索过滤)→详情表单(名称/位置/ 包管理器循环切换/目标预览/中文错误)→创建中;Clone 模式 (URL/名称自动推导/位置)→后台 git clone。 - 名称校验、URL 推导名、starter 命令字符串逐字移植 model.ts; 35 个 newProject.*/welcome.* 中英文案照抄 locale.ts。 - Cargo 新增 rfd 0.16:打开与选择父文件夹走原生目录框。 - view.rs:open_project_path 统一换根(sidebar/maven/关欢迎页); 工具栏与欢迎页的 New/Open/Clone 由占位改为真实链路; 模板项目打开后 starter 命令发往底部终端。 --- linux/Cargo.toml | 1 + linux/src/i18n.rs | 76 ++ linux/src/workbench/mod.rs | 3 + linux/src/workbench/project_dialog.rs | 1344 +++++++++++++++++++++++++ linux/src/workbench/view.rs | 116 ++- 5 files changed, 1513 insertions(+), 27 deletions(-) create mode 100644 linux/src/workbench/project_dialog.rs diff --git a/linux/Cargo.toml b/linux/Cargo.toml index b6aa6d589..dba77ba7f 100644 --- a/linux/Cargo.toml +++ b/linux/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +rfd = "0.16" gpui-kit = "0.6" lithe-core = { path = "../rust/lithe-core" } portable-pty = "0.9" diff --git a/linux/src/i18n.rs b/linux/src/i18n.rs index caf6acd5e..ea3fbc7e1 100644 --- a/linux/src/i18n.rs +++ b/linux/src/i18n.rs @@ -508,6 +508,82 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { "Rules applied when generating commit messages", ), "settings.ai.defaultValue" => ("默认", "Default"), + // ---- 新建项目对话框(与 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) { diff --git a/linux/src/workbench/mod.rs b/linux/src/workbench/mod.rs index ec06815c9..b560769ab 100644 --- a/linux/src/workbench/mod.rs +++ b/linux/src/workbench/mod.rs @@ -3,6 +3,7 @@ pub mod bottom_panel; pub mod command_palette; pub mod editor; pub mod maven; +pub mod project_dialog; pub mod quick_open; pub mod search_everywhere; pub mod settings_dialog; @@ -26,6 +27,8 @@ pub use editor::EditorView; #[allow(unused_imports)] pub use maven::{MavenEvent, MavenView}; #[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}; 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/view.rs b/linux/src/workbench/view.rs index ebf4be423..df3d85149 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -21,6 +21,7 @@ use crate::workbench::bottom_panel::{BottomPanelView, BottomTab}; use crate::workbench::command_palette::{CommandPaletteEvent, CommandPaletteModal}; use crate::workbench::editor::EditorView; use crate::workbench::maven::{MavenEvent, MavenView}; +use crate::workbench::project_dialog::{ProjectDialog, ProjectDialogEvent, ProjectDialogMode}; use crate::workbench::quick_open::{QuickOpenEvent, QuickOpenModal}; use crate::workbench::search_everywhere::{SearchEverywhereEvent, SearchEverywhereModal}; use crate::workbench::settings_dialog::{SettingsCategory, SettingsDialog, SettingsEvent}; @@ -57,6 +58,8 @@ pub struct WorkbenchView { pub show_command_palette: bool, /// 设置模态对话框是否可见 pub show_settings_dialog: bool, + /// 新建/克隆项目对话框是否可见 + pub show_project_dialog: bool, /// 右侧 Maven 导航工具窗口是否可见 pub show_maven: bool, @@ -84,6 +87,8 @@ pub struct WorkbenchView { pub command_palette: Entity, /// 设置模态对话框 pub settings_dialog: Entity, + /// 新建/克隆项目对话框 + pub project_dialog: Entity, /// 欢迎页 pub welcome_screen: Entity, @@ -115,6 +120,7 @@ impl WorkbenchView { let quick_open = cx.new(|cx| QuickOpenModal::new(cx)); let command_palette = cx.new(|cx| CommandPaletteModal::new(cx)); let settings_dialog = cx.new(|cx| SettingsDialog::new(cx)); + let project_dialog = cx.new(|cx| ProjectDialog::new(cx)); let welcome_screen = cx.new(|cx| WelcomeScreenView::new(cx)); let maven = cx.new(|cx| MavenView::new(root.clone(), cx)); let focus_handle = cx.focus_handle(); @@ -386,13 +392,24 @@ impl WorkbenchView { ToolbarEvent::MenuAction(id) => { this.handle_action(id, cx); } - ToolbarEvent::NewProject - | ToolbarEvent::OpenProject - | ToolbarEvent::CloneRepository => { - this.append_log( - "[Project] Project picker is not wired to the backend yet.", - cx, - ); + ToolbarEvent::NewProject => { + let _ = this.project_dialog.update(cx, |d, cx| { + d.set_mode(ProjectDialogMode::New, cx); + }); + this.show_project_dialog = true; + cx.notify(); + } + ToolbarEvent::OpenProject => { + if let Some(dir) = ProjectDialog::pick_folder(None) { + this.open_project_path(dir, cx); + } + } + ToolbarEvent::CloneRepository => { + let _ = this.project_dialog.update(cx, |d, cx| { + d.set_mode(ProjectDialogMode::Clone, cx); + }); + this.show_project_dialog = true; + cx.notify(); } ToolbarEvent::OpenRecent => { this.show_welcome = true; @@ -488,35 +505,56 @@ impl WorkbenchView { }, ); + // 9b. 订阅新建/克隆项目对话框事件 + let sub_project_dialog = cx.subscribe( + &project_dialog, + |this, _dialog, event: &ProjectDialogEvent, cx| match event { + ProjectDialogEvent::OpenedProject { path, starter } => { + this.show_project_dialog = false; + this.open_project_path(path.clone(), cx); + if let Some(cmd) = starter { + let _ = this.bottom_panel.update(cx, |bp, cx| { + bp.set_tab(BottomTab::Terminal, cx); + let _ = bp.terminal.update(cx, |term, cx| { + term.send_command(cmd, cx); + }); + }); + } + cx.notify(); + } + ProjectDialogEvent::Close => { + this.show_project_dialog = false; + cx.notify(); + } + }, + ); + // 10. 订阅欢迎页事件 let sub_welcome = cx.subscribe( &welcome_screen, |this, _welcome, event: &WelcomeEvent, cx| match event { - WelcomeEvent::OpenFolder | WelcomeEvent::NewProject => { - this.append_log( - "[Welcome] Project picker is not wired to the backend yet.", - cx, - ); - } - WelcomeEvent::CloneRepository => { - this.append_log( - "[Welcome] Clone repository is not wired to the backend yet.", - cx, - ); + WelcomeEvent::OpenFolder => { + if let Some(dir) = ProjectDialog::pick_folder(None) { + this.open_project_path(dir, cx); + } } - WelcomeEvent::OpenProject(path) => { - this.workspace_root = path.clone(); - this.show_welcome = false; - let _ = this.sidebar.update(cx, |sb, cx| { - sb.root_path = path.clone(); - sb.refresh(cx); - sb.refresh_git(cx); + WelcomeEvent::NewProject => { + let _ = this.project_dialog.update(cx, |d, cx| { + d.set_mode(ProjectDialogMode::New, cx); }); - let _ = this.maven.update(cx, |m, cx| { - m.set_root(path.clone(), cx); + this.show_project_dialog = true; + cx.notify(); + } + WelcomeEvent::CloneRepository => { + let _ = this.project_dialog.update(cx, |d, cx| { + d.set_mode(ProjectDialogMode::Clone, cx); }); + this.show_project_dialog = true; cx.notify(); } + WelcomeEvent::OpenProject(path) => { + this.open_project_path(path.clone(), cx); + } WelcomeEvent::OpenSettings => { this.open_settings(cx); } @@ -571,6 +609,7 @@ impl WorkbenchView { show_quick_open: false, show_command_palette: false, show_settings_dialog: false, + show_project_dialog: false, show_maven: false, toolbar, activity_rail, @@ -584,6 +623,7 @@ impl WorkbenchView { quick_open, command_palette, settings_dialog, + project_dialog, welcome_screen, focus_handle, client: CoreClient::new(), @@ -598,6 +638,7 @@ impl WorkbenchView { sub_quick_open, sub_command_palette, sub_settings, + sub_project_dialog, sub_welcome, sub_status, sub_appearance, @@ -691,6 +732,24 @@ impl WorkbenchView { cx.notify(); } + /// 切换到指定项目根路径:同步侧边栏、Maven 与欢迎页状态。 + fn open_project_path(&mut self, path: String, cx: &mut Context) { + if self.show_project_dialog { + self.show_project_dialog = false; + } + self.workspace_root = path.clone(); + self.show_welcome = false; + let _ = self.sidebar.update(cx, |sb, cx| { + sb.root_path = path.clone(); + sb.refresh(cx); + sb.refresh_git(cx); + }); + let _ = self.maven.update(cx, |m, cx| { + m.set_root(path.clone(), cx); + }); + cx.notify(); + } + fn append_log(&mut self, message: &str, cx: &mut Context) { let _ = self.bottom_panel.update(cx, |bp, cx| { bp.append_log(message.to_string(), cx); @@ -1070,6 +1129,9 @@ impl Render for WorkbenchView { .when(self.show_settings_dialog, |view| { view.child(self.settings_dialog.clone()) }) + .when(self.show_project_dialog, |view| { + view.child(self.project_dialog.clone()) + }) } } From 6a5fb61e5a925d51d71e2aa2d6bb3bc13f9b1300 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 17:36:35 +0800 Subject: [PATCH 19/59] =?UTF-8?q?chore(linux):=20=E6=9B=B4=E6=96=B0=20Carg?= =?UTF-8?q?o.lock=EF=BC=88rfd=200.16=20=E4=BE=9D=E8=B5=96=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- linux/Cargo.lock | 70 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/linux/Cargo.lock b/linux/Cargo.lock index 4732d81ce..c7ef06208 100644 --- a/linux/Cargo.lock +++ b/linux/Cargo.lock @@ -270,6 +270,28 @@ 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" @@ -2372,7 +2394,7 @@ checksum = "c9e904ee0d351f4e85b967e26ae986d3aa195a8d9d35fcd18184a2c40989694e" dependencies = [ "aho-corasick", "anyhow", - "ashpd", + "ashpd 0.13.13", "async-channel", "chrono", "data-url", @@ -2634,7 +2656,7 @@ dependencies = [ "accesskit_unix", "anyhow", "as-raw-xcb-connection", - "ashpd", + "ashpd 0.13.13", "bitflags 2.13.2", "bytemuck", "calloop", @@ -3885,6 +3907,7 @@ dependencies = [ "gpui-kit", "lithe-core", "portable-pty", + "rfd", "serde", "serde_json", "tokio", @@ -4927,7 +4950,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78f2bfed90f1618b4b48dcad9307f25e14ae894e2949642c87c351601d62cebd" dependencies = [ "aes", - "ashpd", + "ashpd 0.13.13", "async-fs", "async-io", "async-lock", @@ -5998,6 +6021,30 @@ dependencies = [ "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" @@ -7783,8 +7830,15 @@ dependencies = [ "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" @@ -8796,6 +8850,15 @@ 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" @@ -9596,6 +9659,7 @@ dependencies = [ "enumflags2", "serde", "serde_bytes", + "url", "winnow 1.0.4", "zcheapstr", "zvariant_derive", From cb8ccbe9ce71f506244ecd3cff0ea16678140782 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 17:55:56 +0800 Subject: [PATCH 20/59] =?UTF-8?q?feat(linux):=20=E6=9C=80=E8=BF=91?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E8=AE=B0=E5=BD=95=E5=B1=95=E7=A4=BA=E4=B8=8E?= =?UTF-8?q?=E5=88=86=E6=94=AF=E5=BE=BD=E6=A0=87=E7=82=B9=E5=87=BB=E5=88=87?= =?UTF-8?q?=E6=8D=A2=EF=BC=8C=E5=AF=B9=E9=BD=90=20Tauri?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对照 recent-folders.ts / project-name-menu.tsx / GitBranchManager: - 最近项目:Settings 新增 recent_projects(去重首位/过滤不存在/ 截断 12,上限常量对齐 MAX_RECENT_PROJECTS);open_project_path 与 启动时记录;欢迎页列表改读持久化(点击打开/删除同步落盘); 项目胶囊“最近项目”分组渲染条目,点击直达。 - 分支徽标改下拉:分支列表来自 core git.references 本地分支; 当前分支打勾禁用;点其他分支经底部终端 git checkout,2 秒后 刷新 git 状态;分支名为专名不翻译。 - 欢迎页标题/副标题/按钮补 welcome.* 本地化;启动记录抽 settings::record_recent_project 共用。 构建零错误零告警,Xvfb 实测分支检出与最近菜单。 --- linux/src/i18n.rs | 7 + linux/src/settings.rs | 29 ++++ linux/src/workbench/toolbar.rs | 182 +++++++++++++++++++++----- linux/src/workbench/view.rs | 66 +++++++++- linux/src/workbench/welcome_screen.rs | 150 ++++++++++----------- 5 files changed, 316 insertions(+), 118 deletions(-) diff --git a/linux/src/i18n.rs b/linux/src/i18n.rs index ea3fbc7e1..b3a6a1fcf 100644 --- a/linux/src/i18n.rs +++ b/linux/src/i18n.rs @@ -131,6 +131,7 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { "titleProject.cloneRepository" => ("克隆仓库…", "Clone Repository…"), "titleProject.openProjects" => ("打开的项目", "Open Projects"), "titleProject.recentProjects" => ("最近项目", "Recent Projects"), + "titleProject.noRecentProjects" => ("没有最近项目", "No recent projects"), // ---- 侧边栏 explorer 头部 ---- "workbench.project" => ("项目", "Project"), "workbench.changes" => ("更改", "Changes"), @@ -196,6 +197,12 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { "search.noResults" => ("未找到结果", "No results found"), // ---- 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}", diff --git a/linux/src/settings.rs b/linux/src/settings.rs index 0a2866062..016d5ede3 100644 --- a/linux/src/settings.rs +++ b/linux/src/settings.rs @@ -37,6 +37,17 @@ pub const SIDEBAR_BOTTOM_ACTIVITY_ITEM_IDS: &[&str] = &[ /// 状态栏左侧项顺序,对应 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", @@ -164,6 +175,12 @@ pub struct Settings { // 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, @@ -248,6 +265,7 @@ impl Default for Settings { 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()) @@ -357,6 +375,17 @@ pub fn update(cx: &mut App, f: impl FnOnce(&mut Settings)) { 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 { diff --git a/linux/src/workbench/toolbar.rs b/linux/src/workbench/toolbar.rs index 12e805ad5..65abe8909 100644 --- a/linux/src/workbench/toolbar.rs +++ b/linux/src/workbench/toolbar.rs @@ -7,7 +7,7 @@ use gpui_kit::assets::IconName; use gpui_kit::component::button::{Button, ButtonVariants as _}; use gpui_kit::component::menu::{DropdownMenu as _, PopupMenu, PopupMenuItem}; -use gpui_kit::component::{h_flex, Icon, Selectable as _, Sizable 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, Anchor, AnyElement, Context, EventEmitter, InteractiveElement as _, IntoElement, @@ -48,6 +48,10 @@ pub enum ToolbarEvent { OpenProject, CloneRepository, OpenRecent, + /// 打开最近项目中的指定路径(项目胶囊“最近项目”分组条目)。 + OpenRecentProject(String), + /// 检出指定本地分支(分支胶囊下拉条目,对齐 Tauri 分支管理器检出)。 + CheckoutBranch(String), } /// 应用图标:与 Tauri 端 `public/logo.png` 同一文件,编译期嵌入。 @@ -56,6 +60,8 @@ const APP_LOGO_PNG: &[u8] = include_bytes!("../../assets/logo.png"); pub struct ToolbarView { pub workspace_name: String, pub git_branch: Option, + /// 本地分支列表(view 侧由 core `git.references` 同步,供分支胶囊下拉切换)。 + pub branches: Vec, /// 紧凑菜单条是否展开(对齐 Tauri `isCompactMenuVisible`)。 compact_menu_open: bool, /// 解码后的应用图标,项目菜单触发器左侧的徽标(对齐 Tauri 的 `logo.png`)。 @@ -74,6 +80,7 @@ impl ToolbarView { Self { workspace_name: name, git_branch: None, + branches: Vec::new(), compact_menu_open: false, app_logo: std::sync::Arc::new(gpui_kit::Image::from_bytes( gpui_kit::ImageFormat::Png, @@ -86,6 +93,11 @@ impl ToolbarView { self.git_branch = branch; cx.notify(); } + + pub fn set_branches(&mut self, branches: Vec, cx: &mut Context) { + self.branches = branches; + cx.notify(); + } } /// 单个菜单项:i18n 键(`crate::i18n::menu_text`)+ 稳定动作 id + 快捷键展示文本。 @@ -409,6 +421,47 @@ fn event_item( }) } +/// 最近项目条目:目录名主行 + 全路径副行,对齐 Tauri `ProjectMenuRow`。 +fn recent_project_item( + name: String, + path: String, + view: &gpui_kit::Entity, +) -> PopupMenuItem { + let v = view.clone(); + let open_path = path.clone(); + // element 闭包是 Fn,多次调用时 clone 使用。 + PopupMenuItem::element(move |_window, _cx| { + let name = name.clone(); + let path = path.clone(); + v_flex() + .w_full() + .min_w_0() + .child( + div() + .w_full() + .truncate() + .text_xs() + .text_color(ThemeColors::foreground()) + .child(name), + ) + .child( + div() + .w_full() + .truncate() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child(path), + ) + }) + .on_click(move |_, _, cx| { + let path = open_path.clone(); + v.update(cx, |this, cx| { + this.compact_menu_open = false; + cx.emit(ToolbarEvent::OpenRecentProject(path)); + }); + }) +} + /// 按分组把菜单项灌入 `PopupMenu`,分组之间自动插入分隔线。 /// /// 文案在 builder 内用 `cx`(`&mut Context`)实时解析; @@ -486,6 +539,15 @@ impl Render for ToolbarView { let t_clone = crate::i18n::menu_text(cx, "titleProject.cloneRepository").to_string(); let t_open_projects = crate::i18n::menu_text(cx, "titleProject.openProjects").to_string(); let t_recent = crate::i18n::menu_text(cx, "titleProject.recentProjects").to_string(); + let t_no_recent = crate::i18n::menu_text(cx, "titleProject.noRecentProjects").to_string(); + // 最近项目(首位最新,过滤不存在的路径,上限与持久化数组一致)。 + let recents: Vec<(String, String)> = settings::get(cx) + .recent_projects + .iter() + .filter(|p| std::path::Path::new(p).exists()) + .take(settings::MAX_RECENT_PROJECTS) + .map(|p| (settings::project_dir_name(p).to_string(), p.clone())) + .collect(); // 九个顶层菜单标题:render 入口处一次性翻译成 owned String, // 后续 move 闭包只搬运字符串,不再借用 cx(避免 cx 被 move 后再借用)。 @@ -630,40 +692,51 @@ impl Render for ToolbarView { .size(px(14.0)) .text_color(ThemeColors::subtle_foreground()), ) - .dropdown_menu(move |menu, _window, _cx| { - menu.item(event_item( - t_new_project.clone(), - || ToolbarEvent::NewProject, - &v, - )) - .item(event_item(t_open.clone(), || ToolbarEvent::OpenProject, &v)) - .item(event_item( - t_clone.clone(), - || ToolbarEvent::CloneRepository, - &v, - )) - .separator() - // 打开的项目分组标签:只展示,不可点。 - .item(PopupMenuItem::label(t_open_projects.clone())) - .separator() - .item(event_item( - t_recent.clone(), - || ToolbarEvent::OpenRecent, - &v, - )) + .dropdown_menu(move |mut menu, _window, _cx| { + menu = menu + .item(event_item( + t_new_project.clone(), + || ToolbarEvent::NewProject, + &v, + )) + .item(event_item( + t_open.clone(), + || ToolbarEvent::OpenProject, + &v, + )) + .item(event_item( + t_clone.clone(), + || ToolbarEvent::CloneRepository, + &v, + )) + .separator() + // 打开的项目分组标签:只展示,不可点。 + .item(PopupMenuItem::label(t_open_projects.clone())) + .separator() + .item(PopupMenuItem::label(t_recent.clone())); + if recents.is_empty() { + menu = menu.item(PopupMenuItem::label(t_no_recent.clone())); + } else { + for (name, path) in &recents { + menu = menu.item(recent_project_item( + name.clone(), + path.clone(), + &v, + )); + } + } + menu }) }) - .child( - // 分支胶囊:只展示当前分支,不接真实切换 - h_flex() - .items_center() - .gap_1p5() - .px_2() - .py(px(3.0)) - .rounded_md() - .bg(ThemeColors::surface()) - .border_1() - .border_color(ThemeColors::border()) + .child({ + // 分支胶囊:点击展开本地分支下拉(对齐 Tauri 分支管理器), + // 当前分支打勾并禁用,点击其他分支发射 CheckoutBranch 由 view 侧检出。 + let v = view.clone(); + let current = branch.clone(); + let branches = self.branches.clone(); + Button::new("tb-branch-selector") + .small() + .ghost() .child( Icon::new(IconName::GitBranch) .size(px(13.0)) @@ -671,16 +744,53 @@ impl Render for ToolbarView { ) .child( div() + .max_w(px(160.0)) + .truncate() .text_xs() .text_color(ThemeColors::success()) - .child(branch), + .child(branch.clone()), ) .child( Icon::new(IconName::ChevronDown) .size(px(12.0)) .text_color(ThemeColors::subtle_foreground()), - ), - ), + ) + .dropdown_menu(move |mut menu, _window, _cx| { + if branches.is_empty() { + menu = menu.item( + PopupMenuItem::new(current.clone()) + .icon(IconName::Check) + .disabled(true), + ); + } else { + for name in &branches { + if *name == current { + menu = menu.item( + PopupMenuItem::new(name.clone()) + .icon(IconName::Check) + .disabled(true), + ); + } else { + let v = v.clone(); + let target = name.clone(); + menu = menu.item( + PopupMenuItem::new(name.clone()).on_click( + move |_, _, cx| { + let target = target.clone(); + v.update(cx, |_this, cx| { + cx.emit(ToolbarEvent::CheckoutBranch( + target, + )); + }); + }, + ), + ); + } + } + } + menu + }) + }), ) // 右侧:全局搜索图标按钮 + 窗口控件(对齐 Tauri `quickOpenAction` + `WindowControls`) .child( diff --git a/linux/src/workbench/view.rs b/linux/src/workbench/view.rs index df3d85149..5263035ed 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -192,6 +192,43 @@ impl WorkbenchView { sb.set_git_changes(sidebar.read(cx).git_changes.len(), cx); }); + // 同步本地分支列表:core `git.references` 取 kind == local 的 shortName, + // 供顶栏分支胶囊下拉切换(对齐 Tauri 分支管理器列表)。 + let toolbar_branches_sync = toolbar_branch_sync.clone(); + let branch_client = this.client.clone(); + let branch_root = this.workspace_root.clone(); + cx.spawn(async move |_this, cx| { + let task = branch_client.execute::( + &cx, + "git.references", + serde_json::json!({ "root": branch_root }), + ); + if let Ok(val) = task.await { + let mut branches: Vec = 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 _ = toolbar_branches_sync.update(cx, |tb, cx| { + tb.set_branches(branches, cx); + }); + } + }) + .detach(); + // 收集所有文件供全局搜索与快速打开 let mut file_list = Vec::new(); if let Some(root_node) = &sidebar.read(cx).root_node { @@ -415,6 +452,29 @@ impl WorkbenchView { this.show_welcome = true; cx.notify(); } + ToolbarEvent::OpenRecentProject(path) => { + this.open_project_path(path.clone(), cx); + } + ToolbarEvent::CheckoutBranch(branch) => { + // 分支检出走底部 Terminal(分支名双引号包裹防空格/特殊字符), + // 检出异步执行,2 秒后重读 sidebar git 状态刷新分支与变更。 + let _ = this.bottom_panel.update(cx, |bp, cx| { + bp.set_tab(BottomTab::Terminal, cx); + let _ = bp.terminal.update(cx, |term, cx| { + term.send_command(&format!("git checkout \"{branch}\""), cx); + }); + }); + let sidebar = this.sidebar.clone(); + cx.spawn(async move |_this, cx| { + cx.background_executor() + .timer(std::time::Duration::from_secs(2)) + .await; + let _ = sidebar.update(cx, |sb, cx| { + sb.refresh_git(cx); + }); + }) + .detach(); + } }, ); @@ -651,6 +711,8 @@ impl WorkbenchView { !crate::theme::ThemePalette::is_light(&settings::resolved_theme_id(s, false)) }; view.apply_resolved_theme(is_dark, cx); + // 启动目录同样记入最近项目(对齐 Tauri 打开即记录)。 + settings::record_recent_project(cx, &view.workspace_root.clone()); // 构造后初始化 Maven 入口可用性,避免无 pom 目录首次打开仍显示入口。 let maven_has_projects = view.maven.read(cx).has_projects(); let _ = view.plugin_rail.update(cx, |r, cx| { @@ -732,13 +794,15 @@ impl WorkbenchView { cx.notify(); } - /// 切换到指定项目根路径:同步侧边栏、Maven 与欢迎页状态。 + /// 切换到指定项目根路径:同步侧边栏、Maven 与欢迎页状态,并记录最近项目。 fn open_project_path(&mut self, path: String, cx: &mut Context) { if self.show_project_dialog { self.show_project_dialog = false; } self.workspace_root = path.clone(); self.show_welcome = false; + // 记录最近项目(对齐 Tauri upsert)。 + settings::record_recent_project(cx, &path); let _ = self.sidebar.update(cx, |sb, cx| { sb.root_path = path.clone(); sb.refresh(cx); diff --git a/linux/src/workbench/welcome_screen.rs b/linux/src/workbench/welcome_screen.rs index 07704ce11..c12f11e3b 100644 --- a/linux/src/workbench/welcome_screen.rs +++ b/linux/src/workbench/welcome_screen.rs @@ -11,23 +11,13 @@ 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, Context, EventEmitter, FocusHandle, FontWeight, InteractiveElement as _, - IntoElement, KeyDownEvent, ParentElement as _, Render, StatefulInteractiveElement as _, - Styled as _, Window, + div, px, AnyElement, App, Context, EventEmitter, FocusHandle, FontWeight, + InteractiveElement as _, IntoElement, KeyDownEvent, ParentElement as _, Render, + StatefulInteractiveElement as _, Styled as _, Window, }; use crate::theme::ThemeColors; -/// 最近项目条目。 -/// -/// `last_opened` 为展示用的相对时间文案,由上层决定格式,本模块不解析时间。 -#[derive(Debug, Clone)] -pub struct RecentProject { - pub name: String, - pub path: String, - pub last_opened: String, -} - /// 欢迎页对外事件。 #[derive(Debug, Clone)] pub enum WelcomeEvent { @@ -58,8 +48,10 @@ impl WelcomeEvent { } /// 整屏欢迎页:左侧导航栏 + 右侧项目区。 +/// +/// 最近项目直接读 `settings::get(cx).recent_projects`(首位最新),展示时过滤 +/// 不存在的路径;打开与删除都经 `settings::update` 落盘,与 `view.rs` 共用同一数组。 pub struct WelcomeScreenView { - pub recent_projects: Vec, pub selected_index: usize, pub query: String, pub focus_handle: FocusHandle, @@ -70,29 +62,31 @@ impl EventEmitter for WelcomeScreenView {} impl WelcomeScreenView { pub fn new(cx: &mut Context) -> Self { Self { - recent_projects: default_recent_projects(), selected_index: 0, query: String::new(), focus_handle: cx.focus_handle(), } } - /// 当前查询过滤后的最近项目。 - fn visible_projects(&self) -> Vec<&RecentProject> { + /// 当前查询过滤后的最近项目路径(首位最新,不存在的路径已滤除)。 + fn visible_projects(&self, cx: &App) -> Vec { let q = self.query.trim().to_lowercase(); - self.recent_projects + crate::settings::get(cx) + .recent_projects .iter() + .filter(|p| std::path::Path::new(p).exists()) .filter(|p| { - q.is_empty() - || p.name.to_lowercase().contains(&q) - || p.path.to_lowercase().contains(&q) + q.is_empty() || { + let name = crate::settings::project_dir_name(p).to_lowercase(); + name.contains(&q) || p.to_lowercase().contains(&q) + } }) + .cloned() .collect() } /// 当前有效选中下标(对空列表安全)。 - fn current_index(&self) -> usize { - let total = self.visible_projects().len(); + fn current_index(&self, total: usize) -> usize { if total == 0 { 0 } else { @@ -102,16 +96,22 @@ impl WelcomeScreenView { /// 打开可见列表中第 `position` 个项目。 fn open_at(&self, position: usize, cx: &mut Context) { - let visible = self.visible_projects(); - if let Some(project) = visible.get(position) { - cx.emit(WelcomeEvent::OpenProject(project.path.clone())); + let visible = self.visible_projects(cx); + if let Some(path) = visible.get(position) { + cx.emit(WelcomeEvent::OpenProject(path.clone())); } } - /// 从最近列表移除指定路径,并夹紧选中下标。 + /// 从最近列表移除指定路径(同步落盘),并夹紧选中下标。 fn remove_recent(&mut self, path: &str, cx: &mut Context) { - self.recent_projects.retain(|p| p.path != path); - let total = self.visible_projects().len(); + crate::settings::update(cx, |s| { + s.recent_projects.retain(|p| p != path); + }); + let total = crate::settings::get(cx) + .recent_projects + .iter() + .filter(|p| std::path::Path::new(p).exists()) + .count(); if self.selected_index >= total { self.selected_index = total.saturating_sub(1); } @@ -180,7 +180,7 @@ impl WelcomeScreenView { .text_sm() .font_weight(FontWeight::MEDIUM) .text_color(ThemeColors::foreground()) - .child("Projects"), + .child(crate::i18n::menu_text(cx, "welcome.projects")), ), ) .child(div().flex_1()) @@ -203,7 +203,11 @@ impl WelcomeScreenView { .size(px(15.0)) .text_color(ThemeColors::subtle_foreground()), ) - .child(div().text_sm().child("Settings")) + .child( + div() + .text_sm() + .child(crate::i18n::menu_text(cx, "workbench.settings")), + ) .on_click(cx.listener(|_this, _event, _window, cx| { cx.emit(WelcomeEvent::OpenSettings); })), @@ -213,8 +217,8 @@ impl WelcomeScreenView { /// 右侧项目区:标题、搜索与操作按钮、最近项目列表。 fn render_main(&self, cx: &mut Context) -> AnyElement { - let visible = self.visible_projects(); - let current_index = self.current_index(); + let visible = self.visible_projects(cx); + let current_index = self.current_index(visible.len()); v_flex() .flex_1() @@ -229,7 +233,7 @@ impl WelcomeScreenView { .text_xl() .font_weight(FontWeight::BOLD) .text_color(ThemeColors::foreground()) - .child("Welcome to Lithe"), + .child(crate::i18n::menu_text(cx, "welcome.title")), ), ) .child( @@ -237,7 +241,7 @@ impl WelcomeScreenView { div() .text_sm() .text_color(ThemeColors::subtle_foreground()) - .child("Open a project to get started"), + .child(crate::i18n::menu_text(cx, "welcome.openFolderHint")), ), ) .child( @@ -288,7 +292,7 @@ impl WelcomeScreenView { Button::new("welcome-clone") .small() .icon(IconName::GitBranch) - .label("Clone") + .label(crate::i18n::menu_text(cx, "welcome.clone")) .on_click(cx.listener(|_this, _event, _window, cx| { cx.emit(WelcomeEvent::CloneRepository); })), @@ -298,12 +302,23 @@ impl WelcomeScreenView { .small() .primary() .icon(IconName::FolderOpen) - .label("Open") + .label(crate::i18n::menu_text(cx, "welcome.open")) .on_click(cx.listener(|_this, _event, _window, cx| { cx.emit(WelcomeEvent::OpenFolder); })), ), ) + .child( + // 最近项目分组头(对齐 Tauri `welcome.recentProjects`) + div() + .w_full() + .pt_4() + .pb_1() + .text_sm() + .font_weight(FontWeight::MEDIUM) + .text_color(ThemeColors::foreground()) + .child(crate::i18n::menu_text(cx, "welcome.recentProjects")), + ) .child( // 最近项目列表或空态 div() @@ -338,16 +353,17 @@ impl WelcomeScreenView { div() .text_xs() .text_color(ThemeColors::subtle_foreground()) - .child("Open a folder to start working"), + .child(crate::i18n::menu_text( + cx, + "welcome.openFolderHint", + )), ), ) }) - .children(visible.into_iter().enumerate().map(|(position, project)| { + .children(visible.into_iter().enumerate().map(|(position, path)| { let is_selected = position == current_index; - let name = project.name.clone(); - let path = project.path.clone(); - let last_opened = project.last_opened.clone(); - let remove_path = project.path.clone(); + let name = crate::settings::project_dir_name(&path).to_string(); + let remove_path = path.clone(); let badge = project_badge(&name, &path); // 每行使用独立的 hover group,避免兄弟行共享 group 名互相影响 let group_name = format!("welcome-recent-group-{position}"); @@ -380,20 +396,10 @@ impl WelcomeScreenView { .child(name), ) .child( - h_flex() - .gap_2() - .child( - div() - .text_xs() - .text_color(ThemeColors::subtle_foreground()) - .child(path), - ) - .child( - div() - .text_xs() - .text_color(ThemeColors::subtle_foreground()) - .child(last_opened), - ), + div() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child(path), ), ) .child( @@ -410,10 +416,9 @@ impl WelcomeScreenView { crate::i18n::menu_text(cx, "welcome.removeRecent") .replace( "{name}", - remove_path - .rsplit('/') - .next() - .unwrap_or(&remove_path), + crate::settings::project_dir_name( + &remove_path, + ), ), ) .on_click(cx.listener( @@ -455,14 +460,15 @@ impl Render for WelcomeScreenView { } } "down" | "arrowdown" => { - let total = this.visible_projects().len(); + let total = this.visible_projects(cx).len(); if total > 0 && this.selected_index + 1 < total { this.selected_index += 1; cx.notify(); } } "enter" => { - let idx = this.current_index(); + let total = this.visible_projects(cx).len(); + let idx = this.current_index(total); this.open_at(idx, cx); } "backspace" => { @@ -503,24 +509,6 @@ impl Render for WelcomeScreenView { } } -/// 默认示例最近项目。 -/// -/// 使用当前工作目录派生的占位路径,避免写入任何真实用户目录。 -fn default_recent_projects() -> Vec { - let base = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); - let sample = |name: &str, last_opened: &str| RecentProject { - name: name.to_string(), - path: base.join(name).to_string_lossy().to_string(), - last_opened: last_opened.to_string(), - }; - - vec![ - sample("lithe-sandbox", "2 hours ago"), - sample("example-project", "yesterday"), - sample("gpui-playground", "last week"), - ] -} - /// 按名称首字母从语义色板中取色,生成圆角首字母徽标。 fn project_badge(name: &str, path: &str) -> AnyElement { let initial = name From 28efe37863f0201bbf21d3980d08cca122007e24 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 17:56:39 +0800 Subject: [PATCH 21/59] =?UTF-8?q?chore(linux):=20=E5=90=8C=E6=AD=A5=20welc?= =?UTF-8?q?ome=5Fscreen=20=E9=87=8D=E5=AF=BC=E5=87=BA=EF=BC=88RecentProjec?= =?UTF-8?q?t=20=E5=B7=B2=E5=88=A0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- linux/src/workbench/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux/src/workbench/mod.rs b/linux/src/workbench/mod.rs index b560769ab..282778b2a 100644 --- a/linux/src/workbench/mod.rs +++ b/linux/src/workbench/mod.rs @@ -42,4 +42,4 @@ pub use status_bar::{StatusBarEvent, StatusBarView}; pub use toolbar::{ToolbarEvent, ToolbarView}; pub use view::WorkbenchView; #[allow(unused_imports)] -pub use welcome_screen::{RecentProject, WelcomeEvent, WelcomeScreenView}; +pub use welcome_screen::{WelcomeEvent, WelcomeScreenView}; From a42fef08bb52c2b6841d12500a89173885bf841c Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 20:44:04 +0800 Subject: [PATCH 22/59] =?UTF-8?q?feat(linux):=20=E5=B7=A6=E4=B8=8A?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E8=8F=9C=E5=8D=95=E5=8A=A8=E4=BD=9C=E5=85=A8?= =?UTF-8?q?=E9=87=8F=E6=8E=A5=E7=BA=BF=EF=BC=8C=E5=AF=B9=E9=BD=90=20Tauri?= =?UTF-8?q?=20window-menu-bar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 九菜单 File/Edit/View/Go/Terminal/Run/Tools/Window/Help 条目与 Tauri 同序,文案走 i18n - File:新窗口/开文件夹/关文件夹/另存为/全部保存/还原/标签页批量关闭与恢复 - Edit:撤销重做剪贴板全选注释复制行删除行上下移行;无 LSP 后端项禁用 - Go:跳转到行弹窗、上下标签页;无导航历史/LSP 项禁用 - View:换行/行号/空白符开关、字体缩放 10-22 落盘、go_to_line/面板开关 - Terminal:新建会话 respawn、关闭收起;单会话 split 禁用 - Window:全屏 toggle_fullscreen;Help 外链经 xdg-open - 紧凑菜单条去掉 on_mouse_down_out(吞弹窗点击),菜单项可触发 - editor_window_action fallback windows()[0];按键路径 editor_direct 直传 window - 行操作快捷键:Ctrl+D/Ctrl+//Ctrl+Shift+K/Alt+Up-Down - Xvfb 验证:复制行/切换注释/删除行生效 --- linux/src/i18n.rs | 20 + linux/src/workbench/branch_manager.rs | 874 ++++++++++++++++++++++++++ linux/src/workbench/editor.rs | 606 +++++++++++++++++- linux/src/workbench/go_to_line.rs | 219 +++++++ linux/src/workbench/mod.rs | 6 + linux/src/workbench/terminal.rs | 6 + linux/src/workbench/toolbar.rs | 206 +++--- linux/src/workbench/view.rs | 507 +++++++++++++-- 8 files changed, 2283 insertions(+), 161 deletions(-) create mode 100644 linux/src/workbench/branch_manager.rs create mode 100644 linux/src/workbench/go_to_line.rs diff --git a/linux/src/i18n.rs b/linux/src/i18n.rs index b3a6a1fcf..d45b27810 100644 --- a/linux/src/i18n.rs +++ b/linux/src/i18n.rs @@ -183,6 +183,23 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { "notifications.title" => ("通知", "Notifications"), // ---- 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..."), @@ -209,6 +226,9 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { "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"), 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/editor.rs b/linux/src/workbench/editor.rs index 41db6953e..1e51a36e0 100644 --- a/linux/src/workbench/editor.rs +++ b/linux/src/workbench/editor.rs @@ -1,15 +1,16 @@ use gpui_kit::assets::IconName; -use gpui_kit::component::input::{Editor, EditorState, InputEvent}; +use gpui_kit::component::input::{Editor, EditorState, InputEvent, Position}; 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 _, Context, Entity, FontWeight, InteractiveElement as _, - IntoElement, ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, - Subscription, Window, + div, px, relative, AppContext as _, ClipboardItem, Context, Entity, FontWeight, + InteractiveElement as _, IntoElement, ParentElement as _, Render, + StatefulInteractiveElement as _, Styled as _, Subscription, Window, }; use crate::core::CoreClient; +use crate::settings; use crate::theme::ThemeColors; #[derive(Debug, Clone)] @@ -36,6 +37,12 @@ pub struct EditorView { pub encoding: String, /// 当前活动标签对应的上游编辑器状态,随视图生命周期常驻并复用。 editor_state: Entity, + /// 自动换行开关镜像(上游无 getter,本地持有后取反下发)。 + soft_wrap: bool, + /// 行号显示开关镜像。 + show_line_numbers: bool, + /// 空白字符显示开关镜像。 + show_whitespace: bool, /// 已同步到 `editor_state` 的标签索引。 synced_tab: Option, /// 活动标签内容被外部替换(重新打开文件)时置位,渲染时重新灌入编辑器。 @@ -43,11 +50,22 @@ pub struct EditorView { /// 监听编辑器文本变化以回写标签内容与脏标记。 _editor_subscription: Subscription, client: CoreClient, + /// 撤销/重做历史(全文快照,栈顶恒等于编辑器当前值)。 + undo_stack: Vec, + redo_stack: Vec, + /// 已关闭标签(标签快照,原索引),后进先出供恢复。 + closed_stack: Vec<(EditorTab, usize)>, } impl EditorView { pub fn new(workspace_root: String, window: &mut Window, cx: &mut Context) -> Self { - let editor_state = cx.new(|cx| EditorState::new(window, cx).line_number(true)); + 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, @@ -64,10 +82,16 @@ impl EditorView { 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(), } } @@ -108,6 +132,7 @@ impl EditorView { /// 关闭指定索引的标签页 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; @@ -161,10 +186,18 @@ impl EditorView { 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; + 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(); + } cx.notify(); } @@ -202,6 +235,546 @@ impl EditorView { .detach(); } + /// 全文替换并记入撤销历史(`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; + } + for (index, tab) in self.tabs.iter().enumerate() { + self.closed_stack.push((tab.clone(), index)); + } + if self.closed_stack.len() > 30 { + let overflow = self.closed_stack.len() - 30; + self.closed_stack.drain(..overflow); + } + self.tabs.clear(); + self.active_tab_index = None; + 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 mut kept = None; + for (index, tab) in self.tabs.drain(..).enumerate() { + if index == active { + kept = Some(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); + } + if let Some(tab) = kept { + self.tabs.push(tab); + self.active_tab_index = Some(0); + } else { + self.active_tab_index = None; + } + 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 { + 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; + } + for index in 0..active { + 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); + } + self.tabs.drain(..active); + self.active_tab_index = Some(0); + 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; + } + for index in active + 1..self.tabs.len() { + 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); + } + self.tabs.truncate(active + 1); + self.sync_needed = true; + cx.notify(); + } + + /// 恢复最近关闭的标签到原位(后进先出,活动标签指向恢复项)。 + 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") { @@ -230,6 +803,24 @@ impl EditorView { "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("//"), + } + } +} + +/// 行注释风格:行前缀或行级块包裹。 + +/// 行注释风格:行前缀或行级块包裹。 +enum CommentStyle { + Line(&'static str), + Block, } fn is_code_file(name: &str) -> bool { @@ -430,7 +1021,8 @@ impl Render for EditorView { Editor::new(&state) .h(relative(1.0)) .bordered(false) - .readonly(false), + .readonly(false) + .text_size(px(crate::settings::get(cx).font_size)), ) }, ) 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/mod.rs b/linux/src/workbench/mod.rs index 282778b2a..f955ddac6 100644 --- a/linux/src/workbench/mod.rs +++ b/linux/src/workbench/mod.rs @@ -1,7 +1,9 @@ pub mod activity_rail; pub mod bottom_panel; +pub mod branch_manager; pub mod command_palette; pub mod editor; +pub mod go_to_line; pub mod maven; pub mod project_dialog; pub mod quick_open; @@ -21,10 +23,14 @@ pub use activity_rail::{ #[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 go_to_line::{GoToLineEvent, GoToLineModal}; +#[allow(unused_imports)] pub use maven::{MavenEvent, MavenView}; #[allow(unused_imports)] pub use project_dialog::{ProjectDialog, ProjectDialogEvent, ProjectDialogMode}; diff --git a/linux/src/workbench/terminal.rs b/linux/src/workbench/terminal.rs index 2bcc79919..932861752 100644 --- a/linux/src/workbench/terminal.rs +++ b/linux/src/workbench/terminal.rs @@ -131,6 +131,12 @@ impl TerminalView { } cx.notify(); } + + /// 用现有工作目录重建 PTY 会话(失败则置空并通知)。 + pub fn respawn(&mut self, cx: &mut Context) { + self.session = TerminalSession::new(120, 30, &self.working_dir).ok(); + cx.notify(); + } } impl Render for TerminalView { diff --git a/linux/src/workbench/toolbar.rs b/linux/src/workbench/toolbar.rs index 65abe8909..ef87bafd2 100644 --- a/linux/src/workbench/toolbar.rs +++ b/linux/src/workbench/toolbar.rs @@ -7,7 +7,7 @@ use gpui_kit::assets::IconName; use gpui_kit::component::button::{Button, ButtonVariants as _}; use gpui_kit::component::menu::{DropdownMenu as _, PopupMenu, PopupMenuItem}; -use gpui_kit::component::{h_flex, v_flex, Icon, Selectable as _, Sizable as _}; +use gpui_kit::component::{h_flex, v_flex, Disableable as _, Icon, Selectable as _, Sizable as _}; use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::{ div, px, Anchor, AnyElement, Context, EventEmitter, InteractiveElement as _, IntoElement, @@ -50,18 +50,17 @@ pub enum ToolbarEvent { OpenRecent, /// 打开最近项目中的指定路径(项目胶囊“最近项目”分组条目)。 OpenRecentProject(String), - /// 检出指定本地分支(分支胶囊下拉条目,对齐 Tauri 分支管理器检出)。 - CheckoutBranch(String), + /// 打开分支管理器弹窗(分支徽标按钮,对齐 Tauri `GitBranchManager`)。 + OpenBranchManager, } /// 应用图标:与 Tauri 端 `public/logo.png` 同一文件,编译期嵌入。 const APP_LOGO_PNG: &[u8] = include_bytes!("../../assets/logo.png"); pub struct ToolbarView { + pub workspace_root: String, pub workspace_name: String, pub git_branch: Option, - /// 本地分支列表(view 侧由 core `git.references` 同步,供分支胶囊下拉切换)。 - pub branches: Vec, /// 紧凑菜单条是否展开(对齐 Tauri `isCompactMenuVisible`)。 compact_menu_open: bool, /// 解码后的应用图标,项目菜单触发器左侧的徽标(对齐 Tauri 的 `logo.png`)。 @@ -72,15 +71,10 @@ impl EventEmitter for ToolbarView {} impl ToolbarView { pub fn new(workspace_root: &str) -> Self { - let name = workspace_root - .rsplit_once('/') - .map(|(_, n)| n.to_string()) - .unwrap_or_else(|| workspace_root.to_string()); - Self { - workspace_name: name, + workspace_root: workspace_root.to_string(), + workspace_name: workspace_dir_name(workspace_root), git_branch: None, - branches: Vec::new(), compact_menu_open: false, app_logo: std::sync::Arc::new(gpui_kit::Image::from_bytes( gpui_kit::ImageFormat::Png, @@ -94,12 +88,22 @@ impl ToolbarView { cx.notify(); } - pub fn set_branches(&mut self, branches: Vec, cx: &mut Context) { - self.branches = branches; + /// 同步当前 workspace:项目胶囊触发器名称与“打开的项目”行跟随切换。 + pub fn set_workspace_root(&mut self, root: String, cx: &mut Context) { + self.workspace_root = root.clone(); + self.workspace_name = workspace_dir_name(&root); cx.notify(); } } +/// 取 workspace 目录名(项目胶囊触发器与当前项目行主行共用)。 +fn workspace_dir_name(workspace_root: &str) -> String { + workspace_root + .rsplit_once('/') + .map(|(_, n)| n.to_string()) + .unwrap_or_else(|| workspace_root.to_string()) +} + /// 单个菜单项:i18n 键(`crate::i18n::menu_text`)+ 稳定动作 id + 快捷键展示文本。 /// /// 分组与条目顺序严格照抄 Tauri `window-menu-bar.tsx`,`action` 字符串与 @@ -155,7 +159,7 @@ const FILE_MENU: &[&[MenuEntry]] = &[ entry("menu.saveAs", "file.save_as", "Ctrl+Shift+S"), entry("menu.saveAll", "file.save_all", "Ctrl+Alt+S"), entry("menu.revertFile", "file.revert", ""), - entry("menu.showLocalHistory", "file.local_history", ""), + disabled_entry("menu.showLocalHistory", "file.local_history", ""), ], &[ entry("menu.closeTab", "file.close_editor", "Ctrl+W"), @@ -186,21 +190,21 @@ const EDIT_MENU: &[&[MenuEntry]] = &[ entry("menu.find", "edit.find", "Ctrl+F"), entry("menu.findAndReplace", "edit.find_replace", "Ctrl+Alt+F"), entry("menu.toggleComment", "edit.toggle_comment", "Ctrl+/"), - entry("menu.quickFix", "edit.quick_fix", "Ctrl+."), - entry( + disabled_entry("menu.quickFix", "edit.quick_fix", "Ctrl+."), + disabled_entry( "menu.triggerParameterHints", "edit.param_hints", "Ctrl+Shift+Space", ), - entry("menu.showHover", "edit.show_hover", "Ctrl+K Ctrl+I"), + disabled_entry("menu.showHover", "edit.show_hover", "Ctrl+K Ctrl+I"), ], &[ entry("menu.duplicateLine", "edit.duplicate_line", "Ctrl+D"), entry("menu.deleteLine", "edit.delete_line", "Ctrl+Shift+K"), entry("menu.moveLineUp", "edit.move_up", "Alt+Up"), entry("menu.moveLineDown", "edit.move_down", "Alt+Down"), - entry("menu.formatDocument", "edit.format_doc", "Ctrl+Alt+L"), - entry("menu.formatSelection", "edit.format_sel", "Ctrl+K Ctrl+F"), + disabled_entry("menu.formatDocument", "edit.format_doc", "Ctrl+Alt+L"), + disabled_entry("menu.formatSelection", "edit.format_sel", "Ctrl+K Ctrl+F"), ], &[entry( "menu.commandPalette", @@ -235,8 +239,8 @@ const VIEW_MENU: &[&[MenuEntry]] = &[ entry("menu.runAndDebug", "view.show_debug", ""), ], &[ - entry("menu.splitEditor", "view.split_editor", ""), - entry("menu.toggleMinimap", "view.toggle_minimap", ""), + disabled_entry("menu.splitEditor", "view.split_editor", ""), + disabled_entry("menu.toggleMinimap", "view.toggle_minimap", ""), entry("menu.toggleWordWrap", "view.toggle_wrap", "Alt+Z"), entry("menu.toggleLineNumbers", "view.toggle_line_numbers", ""), entry("menu.toggleRenderWhitespace", "view.toggle_whitespace", ""), @@ -255,15 +259,15 @@ const GO_MENU: &[&[MenuEntry]] = &[ entry("menu.goToLine", "go.go_to_line", "Ctrl+G"), ], &[ - entry("menu.goBack", "go.back", "Ctrl+Alt+Left"), - entry("menu.goForward", "go.forward", "Ctrl+Alt+Right"), + disabled_entry("menu.goBack", "go.back", "Ctrl+Alt+Left"), + disabled_entry("menu.goForward", "go.forward", "Ctrl+Alt+Right"), ], &[ - entry("menu.goToDefinition", "go.definition", "F12"), - entry("menu.goToImplementation", "go.implementation", "Ctrl+F12"), - entry("menu.goToTypeDefinition", "go.type_definition", ""), - entry("menu.goToReferences", "go.references", "Ctrl+B"), - entry("menu.renameSymbol", "go.rename", "F2"), + disabled_entry("menu.goToDefinition", "go.definition", "F12"), + disabled_entry("menu.goToImplementation", "go.implementation", "Ctrl+F12"), + disabled_entry("menu.goToTypeDefinition", "go.type_definition", ""), + disabled_entry("menu.goToReferences", "go.references", "Ctrl+B"), + disabled_entry("menu.renameSymbol", "go.rename", "F2"), ], &[ entry("menu.nextTab", "go.next_tab", "Ctrl+Alt+Right"), @@ -274,8 +278,8 @@ const GO_MENU: &[&[MenuEntry]] = &[ // ---- Terminal:对齐 tsx,单组无分隔线 ---- const TERMINAL_MENU: &[&[MenuEntry]] = &[&[ entry("menu.newTerminal", "terminal.new", ""), - entry("menu.splitTerminalRight", "terminal.split_right", "Ctrl+D"), - entry( + disabled_entry("menu.splitTerminalRight", "terminal.split_right", "Ctrl+D"), + disabled_entry( "menu.splitTerminalDown", "terminal.split_down", "Ctrl+Shift+D", @@ -285,15 +289,19 @@ const TERMINAL_MENU: &[&[MenuEntry]] = &[&[ // ---- Run:对齐 tsx,单组无分隔线 ---- const RUN_MENU: &[&[MenuEntry]] = &[&[ - entry("menu.startDebugging", "run.debug_start", "F5"), - entry("menu.stopDebugging", "run.debug_stop", "Shift+F5"), - entry("menu.toggleBreakpoint", "run.breakpoint", "F9"), + disabled_entry("menu.startDebugging", "run.debug_start", "F5"), + disabled_entry("menu.stopDebugging", "run.debug_stop", "Shift+F5"), + disabled_entry("menu.toggleBreakpoint", "run.breakpoint", "F9"), ]]; // ---- Tools:对齐 tsx 三组(首项 databases 禁用,对齐后端能力缺失) ---- const TOOLS_MENU: &[&[MenuEntry]] = &[ &[disabled_entry("menu.databases", "tools.database", "")], - &[entry("menu.webInspector", "tools.inspector", "Ctrl+Alt+I")], + &[disabled_entry( + "menu.webInspector", + "tools.inspector", + "Ctrl+Alt+I", + )], &[ entry("menu.preferences", "tools.settings", ""), entry("menu.keyboardShortcuts", "tools.shortcuts", ""), @@ -312,14 +320,18 @@ const HELP_MENU: &[&[MenuEntry]] = &[ &[ entry("menu.documentation", "help.docs", ""), entry("menu.keyboardShortcuts", "help.shortcuts", ""), - entry("menu.whatsNew", "help.whats_new", ""), + disabled_entry("menu.whatsNew", "help.whats_new", ""), entry("menu.changelog", "help.changelog", ""), ], &[ entry("menu.reportBug", "help.report_bug", ""), entry("menu.requestFeature", "help.feature", ""), ], - &[entry("menu.checkForUpdates", "help.check_updates", "")], + &[disabled_entry( + "menu.checkForUpdates", + "help.check_updates", + "", + )], ]; const APP_MENUS: &[AppMenu] = &[ @@ -462,6 +474,48 @@ fn recent_project_item( }) } +/// 当前项目行:复用最近项目条目的两行样式,右侧加 Check,不可点。 +/// +/// 单根架构下“打开的项目”分组只渲染当前 workspace 这一行,对齐 Tauri +/// `ProjectMenuRow` 当前项(`if (project.isActive) return`,点击无动作)。 +fn current_project_item(name: String, path: String) -> PopupMenuItem { + PopupMenuItem::element(move |_window, _cx| { + let name = name.clone(); + let path = path.clone(); + h_flex() + .w_full() + .items_center() + .gap_2() + .child( + v_flex() + .flex_1() + .min_w_0() + .child( + div() + .w_full() + .truncate() + .text_xs() + .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()), + ) + }) + .disabled(true) +} + /// 按分组把菜单项灌入 `PopupMenu`,分组之间自动插入分隔线。 /// /// 文案在 builder 内用 `cx`(`&mut Context`)实时解析; @@ -540,11 +594,13 @@ impl Render for ToolbarView { let t_open_projects = crate::i18n::menu_text(cx, "titleProject.openProjects").to_string(); let t_recent = crate::i18n::menu_text(cx, "titleProject.recentProjects").to_string(); let t_no_recent = crate::i18n::menu_text(cx, "titleProject.noRecentProjects").to_string(); - // 最近项目(首位最新,过滤不存在的路径,上限与持久化数组一致)。 + // 当前 workspace:打开的项目分组只渲染这一行(单根架构,对齐 Tauri 当前项打 Check)。 + let current_root = self.workspace_root.clone(); + // 最近项目(首位最新,过滤不存在的路径与当前已打开路径,上限与持久化数组一致)。 let recents: Vec<(String, String)> = settings::get(cx) .recent_projects .iter() - .filter(|p| std::path::Path::new(p).exists()) + .filter(|p| p.as_str() != current_root && std::path::Path::new(p).exists()) .take(settings::MAX_RECENT_PROJECTS) .map(|p| (settings::project_dir_name(p).to_string(), p.clone())) .collect(); @@ -580,7 +636,6 @@ impl Render for ToolbarView { )), ) .when(open, move |this| { - let v_out = v_menu.clone(); this.child(gpui_kit::deferred( gpui_kit::anchored() .anchor(Anchor::TopLeft) @@ -600,13 +655,10 @@ impl Render for ToolbarView { .border_1() .border_color(ThemeColors::border()) .shadow_lg() - // 点击菜单条以外区域时收起,对齐 Tauri 紧凑菜单的行为。 - .on_mouse_down_out(move |_, _, cx| { - v_out.update(cx, |this, cx| { - this.compact_menu_open = false; - cx.notify(); - }); - }) + // 紧凑菜单条不挂 `on_mouse_down_out`:下拉弹窗渲染在 + // overlay 层,点击弹窗项会被判为“条外”而在 click + // 完成前销毁弹窗,导致所有菜单项无法触发。 + // 收起由汉堡按钮切换与菜单项自身处理。 .children(APP_MENUS.iter().enumerate().map(|(ix, app)| { let v_item = v_menu.clone(); let key = app.key; @@ -663,6 +715,8 @@ impl Render for ToolbarView { // 项目菜单:应用图标 + 项目名 + ChevronDown,点击展开项目列表 let v = view.clone(); let name = project_name.clone(); + let current_name = project_name.clone(); + let current_root = current_root.clone(); let logo = self.app_logo.clone(); Button::new("tb-project-selector") .small() @@ -710,8 +764,12 @@ impl Render for ToolbarView { &v, )) .separator() - // 打开的项目分组标签:只展示,不可点。 + // 打开的项目分组标签:只展示,不可点;下方单根只渲染当前项目行。 .item(PopupMenuItem::label(t_open_projects.clone())) + .item(current_project_item( + current_name.clone(), + current_root.clone(), + )) .separator() .item(PopupMenuItem::label(t_recent.clone())); if recents.is_empty() { @@ -729,14 +787,13 @@ impl Render for ToolbarView { }) }) .child({ - // 分支胶囊:点击展开本地分支下拉(对齐 Tauri 分支管理器), - // 当前分支打勾并禁用,点击其他分支发射 CheckoutBranch 由 view 侧检出。 - let v = view.clone(); - let current = branch.clone(); - let branches = self.branches.clone(); + // 分支徽标:普通按钮,点击打开分支管理器弹窗 + //(对齐 Tauri `GitBranchManager`);无仓库时禁用。 + let has_repo = self.git_branch.is_some(); Button::new("tb-branch-selector") .small() .ghost() + .disabled(!has_repo) .child( Icon::new(IconName::GitBranch) .size(px(13.0)) @@ -750,46 +807,9 @@ impl Render for ToolbarView { .text_color(ThemeColors::success()) .child(branch.clone()), ) - .child( - Icon::new(IconName::ChevronDown) - .size(px(12.0)) - .text_color(ThemeColors::subtle_foreground()), - ) - .dropdown_menu(move |mut menu, _window, _cx| { - if branches.is_empty() { - menu = menu.item( - PopupMenuItem::new(current.clone()) - .icon(IconName::Check) - .disabled(true), - ); - } else { - for name in &branches { - if *name == current { - menu = menu.item( - PopupMenuItem::new(name.clone()) - .icon(IconName::Check) - .disabled(true), - ); - } else { - let v = v.clone(); - let target = name.clone(); - menu = menu.item( - PopupMenuItem::new(name.clone()).on_click( - move |_, _, cx| { - let target = target.clone(); - v.update(cx, |_this, cx| { - cx.emit(ToolbarEvent::CheckoutBranch( - target, - )); - }); - }, - ), - ); - } - } - } - menu - }) + .on_click(cx.listener(|_this, _event, _window, cx| { + cx.emit(ToolbarEvent::OpenBranchManager); + })) }), ) // 右侧:全局搜索图标按钮 + 窗口控件(对齐 Tauri `quickOpenAction` + `WindowControls`) diff --git a/linux/src/workbench/view.rs b/linux/src/workbench/view.rs index 5263035ed..0eec5d903 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -18,8 +18,10 @@ use crate::workbench::activity_rail::{ ActivityRailEvent, ActivityRailView, PluginActivityRailView, PluginRailEvent, }; use crate::workbench::bottom_panel::{BottomPanelView, BottomTab}; +use crate::workbench::branch_manager::{BranchManagerEvent, BranchManagerView}; use crate::workbench::command_palette::{CommandPaletteEvent, CommandPaletteModal}; use crate::workbench::editor::EditorView; +use crate::workbench::go_to_line::{GoToLineEvent, GoToLineModal}; use crate::workbench::maven::{MavenEvent, MavenView}; use crate::workbench::project_dialog::{ProjectDialog, ProjectDialogEvent, ProjectDialogMode}; use crate::workbench::quick_open::{QuickOpenEvent, QuickOpenModal}; @@ -54,12 +56,16 @@ pub struct WorkbenchView { pub show_search_everywhere: bool, /// 快速打开浮层是否可见 pub show_quick_open: bool, + /// 跳转到行浮层是否可见 + pub show_go_to_line: bool, /// 命令面板浮层是否可见 pub show_command_palette: bool, /// 设置模态对话框是否可见 pub show_settings_dialog: bool, /// 新建/克隆项目对话框是否可见 pub show_project_dialog: bool, + /// 分支管理器弹窗是否可见 + pub show_branch_manager: bool, /// 右侧 Maven 导航工具窗口是否可见 pub show_maven: bool, @@ -83,12 +89,16 @@ pub struct WorkbenchView { pub search_everywhere: Entity, /// 快速打开弹窗 pub quick_open: Entity, + /// 跳转到行弹窗 + pub go_to_line: Entity, /// 命令面板 pub command_palette: Entity, /// 设置模态对话框 pub settings_dialog: Entity, /// 新建/克隆项目对话框 pub project_dialog: Entity, + /// 分支管理器弹窗(对齐 Tauri `GitBranchManager`) + pub branch_manager: Entity, /// 欢迎页 pub welcome_screen: Entity, @@ -118,9 +128,12 @@ impl WorkbenchView { let status_bar = cx.new(|_cx| StatusBarView::new()); let search_everywhere = cx.new(|cx| SearchEverywhereModal::new(cx)); let quick_open = cx.new(|cx| QuickOpenModal::new(cx)); + let go_to_line = cx.new(|cx| GoToLineModal::new(cx)); let command_palette = cx.new(|cx| CommandPaletteModal::new(cx)); let settings_dialog = cx.new(|cx| SettingsDialog::new(cx)); let project_dialog = cx.new(|cx| ProjectDialog::new(cx)); + let branch_manager_root = root.clone(); + let branch_manager = cx.new(|cx| BranchManagerView::new(branch_manager_root, cx)); let welcome_screen = cx.new(|cx| WelcomeScreenView::new(cx)); let maven = cx.new(|cx| MavenView::new(root.clone(), cx)); let focus_handle = cx.focus_handle(); @@ -192,43 +205,6 @@ impl WorkbenchView { sb.set_git_changes(sidebar.read(cx).git_changes.len(), cx); }); - // 同步本地分支列表:core `git.references` 取 kind == local 的 shortName, - // 供顶栏分支胶囊下拉切换(对齐 Tauri 分支管理器列表)。 - let toolbar_branches_sync = toolbar_branch_sync.clone(); - let branch_client = this.client.clone(); - let branch_root = this.workspace_root.clone(); - cx.spawn(async move |_this, cx| { - let task = branch_client.execute::( - &cx, - "git.references", - serde_json::json!({ "root": branch_root }), - ); - if let Ok(val) = task.await { - let mut branches: Vec = 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 _ = toolbar_branches_sync.update(cx, |tb, cx| { - tb.set_branches(branches, cx); - }); - } - }) - .detach(); - // 收集所有文件供全局搜索与快速打开 let mut file_list = Vec::new(); if let Some(root_node) = &sidebar.read(cx).root_node { @@ -455,25 +431,14 @@ impl WorkbenchView { ToolbarEvent::OpenRecentProject(path) => { this.open_project_path(path.clone(), cx); } - ToolbarEvent::CheckoutBranch(branch) => { - // 分支检出走底部 Terminal(分支名双引号包裹防空格/特殊字符), - // 检出异步执行,2 秒后重读 sidebar git 状态刷新分支与变更。 - let _ = this.bottom_panel.update(cx, |bp, cx| { - bp.set_tab(BottomTab::Terminal, cx); - let _ = bp.terminal.update(cx, |term, cx| { - term.send_command(&format!("git checkout \"{branch}\""), cx); - }); + ToolbarEvent::OpenBranchManager => { + let root = this.workspace_root.clone(); + let current = this.sidebar.read(cx).git_branch.clone(); + let _ = this.branch_manager.update(cx, |bm, cx| { + bm.set_repo(root, current, cx); }); - let sidebar = this.sidebar.clone(); - cx.spawn(async move |_this, cx| { - cx.background_executor() - .timer(std::time::Duration::from_secs(2)) - .await; - let _ = sidebar.update(cx, |sb, cx| { - sb.refresh_git(cx); - }); - }) - .detach(); + this.show_branch_manager = true; + cx.notify(); } }, ); @@ -535,6 +500,25 @@ impl WorkbenchView { }, ); + // 7b. 订阅跳转到行事件 + let sub_go_to_line = cx.subscribe( + &go_to_line, + move |this, _modal, event: &GoToLineEvent, cx| match event { + GoToLineEvent::Confirm(line) => { + let line = *line; + this.show_go_to_line = false; + this.editor_window_action(cx, move |ed, window, cx| { + ed.go_to_line(line, window, cx); + }); + cx.notify(); + } + GoToLineEvent::Close => { + this.show_go_to_line = false; + cx.notify(); + } + }, + ); + // 8. 订阅命令面板事件 let sub_command_palette = cx.subscribe( &command_palette, @@ -589,6 +573,30 @@ impl WorkbenchView { }, ); + // 9c. 订阅分支管理器弹窗事件 + let sub_branch_manager = cx.subscribe( + &branch_manager, + |this, _bm, event: &BranchManagerEvent, cx| match event { + BranchManagerEvent::CheckoutDone => { + // core 直连已完成才返回:关弹窗并重读 sidebar git 状态, + // 分支徽标经既有 observer 同步刷新。 + this.show_branch_manager = false; + let _ = this.sidebar.update(cx, |sb, cx| { + sb.refresh_git(cx); + }); + cx.notify(); + } + BranchManagerEvent::OpenWorktree(path) => { + this.show_branch_manager = false; + this.open_project_path(path.clone(), cx); + } + BranchManagerEvent::Close => { + this.show_branch_manager = false; + cx.notify(); + } + }, + ); + // 10. 订阅欢迎页事件 let sub_welcome = cx.subscribe( &welcome_screen, @@ -667,9 +675,11 @@ impl WorkbenchView { show_welcome: false, show_search_everywhere: false, show_quick_open: false, + show_go_to_line: false, show_command_palette: false, show_settings_dialog: false, show_project_dialog: false, + show_branch_manager: false, show_maven: false, toolbar, activity_rail, @@ -681,9 +691,11 @@ impl WorkbenchView { status_bar, search_everywhere, quick_open, + go_to_line, command_palette, settings_dialog, project_dialog, + branch_manager, welcome_screen, focus_handle, client: CoreClient::new(), @@ -696,9 +708,11 @@ impl WorkbenchView { sub_toolbar, sub_search, sub_quick_open, + sub_go_to_line, sub_command_palette, sub_settings, sub_project_dialog, + sub_branch_manager, sub_welcome, sub_status, sub_appearance, @@ -713,6 +727,11 @@ impl WorkbenchView { view.apply_resolved_theme(is_dark, cx); // 启动目录同样记入最近项目(对齐 Tauri 打开即记录)。 settings::record_recent_project(cx, &view.workspace_root.clone()); + // 同步工具栏当前项目行(仿分支同步写法;构造时已传入,此处再确认一次)。 + let root_for_toolbar = view.workspace_root.clone(); + let _ = view.toolbar.update(cx, |tb, cx| { + tb.set_workspace_root(root_for_toolbar.clone(), cx); + }); // 构造后初始化 Maven 入口可用性,避免无 pom 目录首次打开仍显示入口。 let maven_has_projects = view.maven.read(cx).has_projects(); let _ = view.plugin_rail.update(cx, |r, cx| { @@ -801,6 +820,10 @@ impl WorkbenchView { } self.workspace_root = path.clone(); self.show_welcome = false; + // 同步工具栏当前项目行(仿分支同步写法)。 + let _ = self.toolbar.update(cx, |tb, cx| { + tb.set_workspace_root(path.clone(), cx); + }); // 记录最近项目(对齐 Tauri upsert)。 settings::record_recent_project(cx, &path); let _ = self.sidebar.update(cx, |sb, cx| { @@ -840,6 +863,15 @@ impl WorkbenchView { cx.notify(); } + /// 打开跳转到行弹窗 + pub fn open_go_to_line(&mut self, cx: &mut Context) { + self.show_go_to_line = true; + let _ = self.go_to_line.update(cx, |modal, cx| { + modal.reset(cx); + }); + cx.notify(); + } + /// 打开命令面板 pub fn open_command_palette(&mut self, cx: &mut Context) { self.show_command_palette = true; @@ -860,6 +892,144 @@ impl WorkbenchView { } } + /// 持有 `&mut Window` 调用方的编辑器动作直达通道(按键监听等已有 + /// window 的路径使用,避免经窗口句柄二次 `update` 的重入借用失败)。 + fn editor_direct( + &mut self, + window: &mut Window, + cx: &mut Context, + action: impl FnOnce(&mut EditorView, &mut Window, &mut Context), + ) { + let _ = self.editor.update(cx, |ed, cx| action(ed, window, cx)); + } + + /// 无 `window` 上下文的编辑器动作经窗口句柄下发(菜单/面板事件路径, + /// 非重入场景)。`active_window` 仅反映平台聚焦,Xvfb 下可能为 None, + /// 单窗口应用退化到 `windows()` 首个句柄。 + fn editor_window_action( + &self, + cx: &mut Context, + action: impl FnOnce(&mut EditorView, &mut Window, &mut Context) + 'static, + ) { + let editor = self.editor.clone(); + let handle = cx + .active_window() + .or_else(|| cx.windows().into_iter().next()); + if let Some(handle) = handle { + let _ = handle.update(cx, |_, window, cx| { + editor.update(cx, |ed, cx| action(ed, window, cx)); + }); + } + } + + /// 在系统浏览器中打开 URL(Linux-frame:`xdg-open`,失败忽略)。 + fn open_url(url: &str) { + let _ = std::process::Command::new("xdg-open").arg(url).spawn(); + } + + /// 另存为:存盘框选目标后落盘,并更新活动标签的 path/title。 + /// + /// 目标在工作区内时经 core `file.write`(仿 `save_active`);工作区外 + /// core 会拒绝绝对路径,改用 `std::fs` 直写。 + fn save_active_as(&mut self, cx: &mut Context) { + let (old_path, text) = { + let ed = self.editor.read(cx); + let Some(idx) = ed.active_tab_index else { + return; + }; + let Some(tab) = ed.tabs.get(idx) else { + return; + }; + (tab.path.clone(), tab.content.clone()) + }; + let default_name = old_path + .rsplit('/') + .next() + .unwrap_or("untitled.txt") + .to_string(); + let dest = rfd::FileDialog::new() + .set_directory(&self.workspace_root) + .set_file_name(&default_name) + .save_file(); + let Some(dest) = dest else { + return; + }; + let title = dest + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or(default_name); + let root = self.workspace_root.clone(); + if let Ok(rel) = dest.strip_prefix(std::path::Path::new(&root)) { + let rel = rel.to_string_lossy().to_string(); + let editor = self.editor.clone(); + let client = self.client.clone(); + cx.spawn(async move |_this, cx| { + if client.write_file(&cx, &root, &rel, &text).await.is_ok() { + let _ = editor.update(cx, |ed, cx| { + if let Some(idx) = ed.active_tab_index { + if let Some(t) = ed.tabs.get_mut(idx) { + if t.path == old_path { + t.path = rel.clone(); + t.title = title.clone(); + t.is_dirty = false; + cx.notify(); + } + } + } + }); + } + }) + .detach(); + } else if std::fs::write(&dest, text.as_bytes()).is_ok() { + let abs = dest.to_string_lossy().to_string(); + let _ = self.editor.update(cx, |ed, cx| { + if let Some(idx) = ed.active_tab_index { + if let Some(t) = ed.tabs.get_mut(idx) { + if t.path == old_path { + t.path = abs; + t.title = title; + t.is_dirty = false; + cx.notify(); + } + } + } + }); + } + } + + /// 还原文件:重读活动文件内容并灌回编辑器(同路径 `open_file` 会刷新内容并清除脏标记)。 + fn revert_active_file(&mut self, cx: &mut Context) { + let path = { + let ed = self.editor.read(cx); + let Some(idx) = ed.active_tab_index else { + return; + }; + let Some(tab) = ed.tabs.get(idx) else { + return; + }; + tab.path.clone() + }; + if std::path::Path::new(&path).is_absolute() { + if let Ok(text) = std::fs::read_to_string(&path) { + let _ = self.editor.update(cx, |ed, cx| { + ed.open_file(path, text, cx); + }); + } + return; + } + let root = self.workspace_root.clone(); + let editor = self.editor.clone(); + let client = self.client.clone(); + cx.spawn(async move |_this, cx| { + if let Ok(text) = client.read_file(&cx, &root, &path).await { + let _ = editor.update(cx, |ed, cx| { + ed.open_file(path, text, cx); + }); + } + }) + .detach(); + } + /// 执行常用命令动作(工具栏菜单、命令面板、全局搜索共用)。 pub fn handle_action(&mut self, action_id: &str, cx: &mut Context) { // 主题子菜单:menu.theme.,直接写设置并应用主题。 @@ -897,7 +1067,7 @@ impl WorkbenchView { } }); } - "workbench.toggle_terminal" | "view.toggle_bottom_panel" | "terminal.new" => { + "workbench.toggle_terminal" | "view.toggle_bottom_panel" => { let _ = self.bottom_panel.update(cx, |bp, cx| { bp.toggle_collapsed(cx); }); @@ -953,7 +1123,7 @@ impl WorkbenchView { cx, ); } - "file.exit" | "window.close" => { + "file.exit" | "window.close" | "file.close_window" => { cx.quit(); } "window.minimize" => { @@ -1010,12 +1180,190 @@ impl WorkbenchView { self.append_log("[Run and Debug] Debug panel is not wired yet.", cx); } // 快捷键设置:设置对话框切到 Keyboard 分类并打开。 - "tools.shortcuts" => { + "tools.shortcuts" | "help.shortcuts" => { let _ = self.settings_dialog.update(cx, |d, cx| { d.set_category(SettingsCategory::Keyboard, cx); }); self.open_settings(cx); } + // ---- File:新窗口 / 文件夹 / 另存为 / 标签页批量操作 ---- + "file.new_window" => { + if let Ok(exe) = std::env::current_exe() { + let _ = std::process::Command::new(exe).spawn(); + } + } + "file.open_folder" => { + if let Some(dir) = ProjectDialog::pick_folder(None) { + self.open_project_path(dir, cx); + } + } + "file.close_folder" => { + self.show_welcome = true; + cx.notify(); + } + "file.save_as" => { + self.save_active_as(cx); + } + "file.save_all" => { + let _ = self.editor.update(cx, |ed, cx| { + ed.save_all_tabs(cx); + }); + } + "file.revert" => { + self.revert_active_file(cx); + } + "file.close_all" => { + let _ = self.editor.update(cx, |ed, cx| { + ed.close_all_tabs(cx); + }); + } + "file.close_others" => { + let _ = self.editor.update(cx, |ed, cx| { + ed.close_other_tabs(cx); + }); + } + "file.close_saved" => { + let _ = self.editor.update(cx, |ed, cx| { + ed.close_saved_tabs(cx); + }); + } + "file.close_left" => { + let _ = self.editor.update(cx, |ed, cx| { + ed.close_tabs_to_left(cx); + }); + } + "file.close_right" => { + let _ = self.editor.update(cx, |ed, cx| { + ed.close_tabs_to_right(cx); + }); + } + "file.reopen_closed" => { + let _ = self.editor.update(cx, |ed, cx| { + ed.reopen_closed_tab(cx); + }); + } + // ---- Edit:撤销/剪贴板/行操作(需 window 的经 defer 下发) ---- + "edit.undo" => { + self.editor_window_action(cx, |ed, window, cx| ed.undo(window, cx)); + } + "edit.redo" => { + self.editor_window_action(cx, |ed, window, cx| ed.redo(window, cx)); + } + "edit.cut" => { + self.editor_window_action(cx, |ed, window, cx| ed.cut(window, cx)); + } + "edit.copy" => { + let _ = self.editor.update(cx, |ed, cx| { + ed.copy(cx); + }); + } + "edit.paste" => { + self.editor_window_action(cx, |ed, window, cx| ed.paste(window, cx)); + } + "edit.select_all" => { + self.editor_window_action(cx, |ed, window, cx| ed.select_all(window, cx)); + } + "edit.toggle_comment" => { + self.editor_window_action(cx, |ed, window, cx| ed.toggle_comment(window, cx)); + } + "edit.duplicate_line" => { + self.editor_window_action(cx, |ed, window, cx| ed.duplicate_line(window, cx)); + } + "edit.delete_line" => { + self.editor_window_action(cx, |ed, window, cx| ed.delete_line(window, cx)); + } + "edit.move_up" => { + self.editor_window_action(cx, |ed, window, cx| ed.move_line_up(window, cx)); + } + "edit.move_down" => { + self.editor_window_action(cx, |ed, window, cx| ed.move_line_down(window, cx)); + } + // ---- Go:跳转到行 / 标签页切换 ---- + "go.go_to_line" => { + self.open_go_to_line(cx); + } + "go.next_tab" => { + let _ = self.editor.update(cx, |ed, cx| { + ed.goto_next_tab(cx); + }); + } + "go.prev_tab" => { + let _ = self.editor.update(cx, |ed, cx| { + ed.goto_prev_tab(cx); + }); + } + // ---- View:显示开关 / 缩放 ---- + "view.toggle_wrap" => { + self.editor_window_action(cx, |ed, window, cx| ed.toggle_wrap(window, cx)); + } + "view.toggle_line_numbers" => { + self.editor_window_action(cx, |ed, window, cx| ed.toggle_line_numbers(window, cx)); + } + "view.toggle_whitespace" => { + self.editor_window_action(cx, |ed, window, cx| ed.toggle_whitespace(window, cx)); + } + "view.zoom_in" => { + settings::update(cx, |s| s.font_size = (s.font_size + 1.0).min(22.0)); + let msg = format!( + "[View] Font size: {} px", + settings::get(cx).font_size as i32 + ); + self.append_log(&msg, cx); + cx.notify(); + } + "view.zoom_out" => { + settings::update(cx, |s| s.font_size = (s.font_size - 1.0).max(10.0)); + let msg = format!( + "[View] Font size: {} px", + settings::get(cx).font_size as i32 + ); + self.append_log(&msg, cx); + cx.notify(); + } + "view.reset_zoom" => { + settings::update(cx, |s| s.font_size = 14.0); + self.append_log("[View] Font size reset to 14 px", cx); + cx.notify(); + } + // ---- Terminal:新建会话 / 关闭面板(只关不开) ---- + "terminal.new" => { + let _ = self.bottom_panel.update(cx, |bp, cx| { + bp.set_tab(BottomTab::Terminal, cx); + let _ = bp.terminal.update(cx, |term, cx| { + term.respawn(cx); + }); + }); + } + "terminal.close" => { + if !self.bottom_panel.read(cx).is_collapsed { + let _ = self.bottom_panel.update(cx, |bp, cx| { + bp.toggle_collapsed(cx); + }); + } + } + // ---- Window:全屏(gpui-pre `Window::toggle_fullscreen`) ---- + "window.fullscreen" => { + cx.defer(|cx| { + if let Some(handle) = cx.active_window() { + let _ = handle.update(cx, |_, window, _| window.toggle_fullscreen()); + } + }); + } + // ---- Help:外部链接经 `xdg-open` 打开 ---- + "help.docs" => { + Self::open_url("https://lithe.top/docs"); + } + "help.changelog" => { + Self::open_url("https://github.com/1lck/Lithe-IDEA/releases"); + } + "help.report_bug" => { + Self::open_url("https://github.com/1lck/Lithe-IDEA/issues/new?template=01-bug.yml"); + } + "help.feature" => { + Self::open_url( + "https://github.com/1lck/Lithe-IDEA/issues/new?template=02-feature.yml", + ); + } _ => {} } } @@ -1092,16 +1440,20 @@ impl Render for WorkbenchView { .relative() .size_full() .bg(ThemeColors::background()) - .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { + .on_key_down(cx.listener(|this, event: &KeyDownEvent, window, cx| { let key = event.keystroke.key.as_str(); let modifiers = event.keystroke.modifiers; // 浮层优先:Esc 关闭最上层模态。 if key == "escape" { - if this.show_command_palette { + if this.show_branch_manager { + this.show_branch_manager = false; + } else if this.show_command_palette { this.show_command_palette = false; } else if this.show_quick_open { this.show_quick_open = false; + } else if this.show_go_to_line { + this.show_go_to_line = false; } else if this.show_search_everywhere { this.show_search_everywhere = false; } else if this.show_settings_dialog { @@ -1134,7 +1486,34 @@ impl Render for WorkbenchView { bp.set_tab(BottomTab::Terminal, cx); }); } - _ => {} + // 行操作快捷键与菜单展示一致(上游编辑器不原生支持)。 + // 按键监听自带 `window`,直接下发,避免经句柄二次 + // `update` 的重入借用失败。 + "d" => this.editor_direct(window, cx, |ed, window, cx| { + ed.duplicate_line(window, cx); + }), + "/" => this.editor_direct(window, cx, |ed, window, cx| { + ed.toggle_comment(window, cx); + }), + _ => { + if modifiers.shift && (key == "k" || key == "K") { + this.editor_direct(window, cx, |ed, window, cx| { + ed.delete_line(window, cx); + }); + } + } + } + // Alt+Up/Down 移动行(`modifiers.alt`)。 + if modifiers.alt && (key == "up" || key == "down") { + if key == "up" { + this.editor_direct(window, cx, |ed, window, cx| { + ed.move_line_up(window, cx); + }); + } else { + this.editor_direct(window, cx, |ed, window, cx| { + ed.move_line_down(window, cx); + }); + } } } })) @@ -1187,6 +1566,9 @@ impl Render for WorkbenchView { .when(self.show_quick_open, |view| { view.child(self.quick_open.clone()) }) + .when(self.show_go_to_line, |view| { + view.child(self.go_to_line.clone()) + }) .when(self.show_command_palette, |view| { view.child(self.command_palette.clone()) }) @@ -1196,6 +1578,9 @@ impl Render for WorkbenchView { .when(self.show_project_dialog, |view| { view.child(self.project_dialog.clone()) }) + .when(self.show_branch_manager, |view| { + view.child(self.branch_manager.clone()) + }) } } From dc6d2db25a53de5b2a5ab3f0561ce454d075f76a Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 21:28:08 +0800 Subject: [PATCH 23/59] =?UTF-8?q?feat(linux):=20=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=E5=85=A8=E9=83=A8=E5=88=86=E7=B1=BB=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E6=8E=A5=E7=BA=BF=EF=BC=8C=E5=AF=B9=E9=BD=90=20Tauri/?= =?UTF-8?q?Mac?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Settings 补 Tauri 字段:fontFamily/lineHeight/renderWhitespace/vim/formatOnSave/terminalFontSize/cursorBlink/uiFontSize/gitExecutable/gitUseCredentialHelper/aiProvider/aiModel/aiCompletion/aiCommit/customLogDirectory;fontSize 默认 14 - General:隐藏目录/文件改为 Textarea 可编辑,应用按钮解析落盘 - Editor:字体下拉/行高步进/空白符下拉/字号 8-32,新增行为分组(换行/Vim/保存格式化);编辑器每帧同步 wrap/行号/空白符 - Keyboard:预设补全 8 项,Vim 开关 - Terminal:Linux 真实 shell 探测下拉,字号/回滚量/光标闪烁;终端按设置 shell 建会话、字号渲染 - LSP:保存时格式化开关 - Project:工具链三行可编辑,which 探测回填,保存写 .lithe/run/local.json - Run:读 configurations.json 列表,生成默认 Java 配置 - AI:提供商下拉/模型输入应用/自动补全开关;AI 提交:启用/语言/格式/正文/标题长度/diff 上限 - Git:执行分组(可执行路径探测清空应用+凭据助手开关) - Logs:自定义目录选择应用、清理 *.log、导出 diagnostic-bundle.json - Updates:检查更新显示已是最新版本 - Xvfb 实测:生成/保存/步进/探测/选择/导出/检查/应用均落盘生效 --- linux/src/i18n.rs | 33 + linux/src/settings.rs | 63 +- linux/src/workbench/editor.rs | 26 + linux/src/workbench/settings_dialog.rs | 1438 ++++++++++++++++++++---- linux/src/workbench/terminal.rs | 45 +- 5 files changed, 1382 insertions(+), 223 deletions(-) diff --git a/linux/src/i18n.rs b/linux/src/i18n.rs index d45b27810..1f1e12a5f 100644 --- a/linux/src/i18n.rs +++ b/linux/src/i18n.rs @@ -323,6 +323,17 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { "在主视图中将打开的缓冲区显示为可横向滚动的轮播", "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"), @@ -341,6 +352,9 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { "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" => ( @@ -365,6 +379,7 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { "选择 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"), @@ -432,6 +447,11 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { "在编辑器中显示当前行的 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" => ( @@ -477,6 +497,8 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { "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" => ( "本次会话启用诊断日志", @@ -508,6 +530,7 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { "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.", @@ -515,6 +538,7 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { // ---- 设置对话框内容:项目工具链 / 运行生成 / 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" => ( "完整提供商与模型选择尚未接入,后续版本提供。", @@ -535,6 +559,15 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { "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"), diff --git a/linux/src/settings.rs b/linux/src/settings.rs index 016d5ede3..1a10b668d 100644 --- a/linux/src/settings.rs +++ b/linux/src/settings.rs @@ -123,6 +123,31 @@ pub const DEFAULT_HIDDEN_FILE_PATTERNS: &[&str] = &[ "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, + } + } +} + /// 应用设置真源,字段名与 Tauri `defaultSettings` 的 camelCase 键一一对应。 #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] @@ -131,16 +156,24 @@ pub struct Settings { 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, // UI + pub ui_font_size: f32, pub display_language: String, pub reduce_motion: bool, pub show_status_bar: bool, @@ -168,7 +201,9 @@ pub struct Settings { 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, @@ -210,6 +245,15 @@ pub struct Settings { 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, @@ -220,14 +264,22 @@ impl Default for Settings { Self { auto_save: true, quick_open_preview: true, - font_size: 13.0, + 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(), + ui_font_size: 13.0, display_language: "zh-CN".to_string(), reduce_motion: false, show_status_bar: true, @@ -260,6 +312,8 @@ impl Default for Settings { 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, @@ -299,6 +353,13 @@ impl Default for Settings { 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(), } diff --git a/linux/src/workbench/editor.rs b/linux/src/workbench/editor.rs index 1e51a36e0..3bdfbc82a 100644 --- a/linux/src/workbench/editor.rs +++ b/linux/src/workbench/editor.rs @@ -852,6 +852,32 @@ fn is_code_file(name: &str) -> bool { 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 diff --git a/linux/src/workbench/settings_dialog.rs b/linux/src/workbench/settings_dialog.rs index c6cb868d7..a67fdbcd5 100644 --- a/linux/src/workbench/settings_dialog.rs +++ b/linux/src/workbench/settings_dialog.rs @@ -9,13 +9,15 @@ 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, Context, EventEmitter, FontWeight, InteractiveElement as _, IntoElement, - ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Window, + div, px, rgba, AppContext as _, Context, Entity, EventEmitter, FontWeight, + InteractiveElement as _, IntoElement, ParentElement as _, Render, + StatefulInteractiveElement as _, Styled as _, Window, }; use crate::settings::{self, Settings}; @@ -149,6 +151,27 @@ pub struct SettingsDialog { 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, + /// 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 {} @@ -167,6 +190,20 @@ impl SettingsDialog { 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(), + ai_model_input: None, + git_exe_input: None, + log_dir_input: None, + logs_status: String::new(), + updates_status: String::new(), } } @@ -187,6 +224,20 @@ impl SettingsDialog { 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.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` 并刷新视图。 @@ -204,10 +255,49 @@ impl SettingsDialog { 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 { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { // 全屏半透明遮罩:点击遮罩关闭,点击卡片不冒泡。 div() .id("settings-dialog-backdrop") @@ -358,10 +448,10 @@ impl Render for SettingsDialog { ) .child(match self.active_category { SettingsCategory::General => self - .render_general_content(cx) + .render_general_content(window, cx) .into_any_element(), SettingsCategory::Project => self - .render_project_content(cx) + .render_project_content(window, cx) .into_any_element(), SettingsCategory::Run => { self.render_run_content(cx).into_any_element() @@ -378,18 +468,18 @@ impl Render for SettingsDialog { SettingsCategory::Lsp => { self.render_lsp_content(cx).into_any_element() } - SettingsCategory::Ai => { - self.render_ai_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(cx).into_any_element() - } - SettingsCategory::Logs => { - self.render_logs_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(), @@ -706,22 +796,6 @@ impl SettingsDialog { }) } - /// 多行只读文本块:隐藏路径等模式列表展示用(编辑能力后续接入)。 - fn render_text_block(&self, text: String, min_height: f32) -> impl IntoElement { - div() - .w_full() - .min_h(px(min_height)) - .p_2() - .rounded_sm() - .border_1() - .border_color(ThemeColors::border()) - .bg(ThemeColors::background()) - .font_family("monospace") - .text_xs() - .text_color(ThemeColors::muted_foreground()) - .child(text) - } - /// 分组内的说明文本。 fn render_note(&self, text: String) -> impl IntoElement { div() @@ -734,8 +808,25 @@ impl SettingsDialog { /// 各分类内容渲染。 impl SettingsDialog { /// 常规:对齐 Tauri `GeneralPanel`(外观/语言/项目/文件/Git/隐藏路径)。 - fn render_general_content(&self, cx: &mut Context) -> impl IntoElement { + fn render_general_content( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { let s = settings::get(cx).clone(); + // 隐藏路径编辑器先创建(避免 render_group 的 &self 借用冲突)。 + let dirs_entity = Self::ensure_textarea( + &mut self.hidden_dirs_input, + &s.hidden_directory_patterns.join("\n"), + window, + cx, + ); + let files_entity = Self::ensure_textarea( + &mut self.hidden_files_input, + &s.hidden_file_patterns.join("\n"), + window, + cx, + ); // 外观模式由 syncSystemTheme + theme 派生,与 Tauri `appearanceMode` 一致。 let appearance_mode = if s.sync_system_theme { "system" @@ -980,16 +1071,24 @@ impl SettingsDialog { .to_string(), ), ) - .child(v_flex().w_full().gap_1p5().child( - div().text_xs().text_color(ThemeColors::foreground()).child( - crate::i18n::menu_text(cx, "settings.mac.directories").to_string(), - ), + .child(div().text_xs().text_color(ThemeColors::foreground()).child( + crate::i18n::menu_text(cx, "settings.mac.directories").to_string(), )) - .child(self.render_text_block(s.hidden_directory_patterns.join("\n"), 72.0)) + .child({ + div() + .w_full() + .h(px(72.0)) + .child(Textarea::new(&dirs_entity).bordered(true)) + }) .child(div().text_xs().text_color(ThemeColors::foreground()).child( crate::i18n::menu_text(cx, "settings.mac.filePatterns").to_string(), )) - .child(self.render_text_block(s.hidden_file_patterns.join("\n"), 56.0)) + .child({ + div() + .w_full() + .h(px(56.0)) + .child(Textarea::new(&files_entity).bordered(true)) + }) .child( h_flex().w_full().justify_end().child( Button::new("general-apply-patterns") @@ -999,9 +1098,29 @@ impl SettingsDialog { crate::i18n::menu_text(cx, "settings.mac.apply") .to_string(), ) - .on_click(cx.listener(|_this, _event, _window, cx| { - // 模式编辑器后续接入;当前落盘值即显示值。 - cx.notify(); + .on_click(cx.listener(|this, _event, _window, cx| { + // 每行一个 glob:trim + 去空后落盘,即时影响文件树。 + let dirs = this + .hidden_dirs_input + .as_ref() + .map(|e| e.read(cx).value().to_string()) + .unwrap_or_default(); + let files = this + .hidden_files_input + .as_ref() + .map(|e| e.read(cx).value().to_string()) + .unwrap_or_default(); + let split = |text: String| { + text.lines() + .map(str::trim) + .filter(|l| !l.is_empty()) + .map(str::to_string) + .collect::>() + }; + this.commit(cx, |s| { + s.hidden_directory_patterns = split(dirs); + s.hidden_file_patterns = split(files); + }) })), ), ), @@ -1009,10 +1128,52 @@ impl SettingsDialog { ) } - /// 项目 · JDK 与 Maven:对齐 `ProjectEnvironmentSettings` 的字段结构 - /// (工作区路径、作用域说明、三个工具链路径行、保存按钮)。 - /// 路径探测与保存尚未接入后端,输入框为只读占位。 - fn render_project_content(&self, cx: &mut Context) -> impl IntoElement { + /// 项目 · JDK 与 Maven:对齐 `ProjectEnvironmentSettings`(工作区路径、 + /// 工具链三行、保存按钮)。值来自 `/.lithe/run/local.json` + ///(对齐 Mac 本机配置),缺失时回退 `which java/mvn` 探测;保存写回该文件。 + fn render_project_content( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { + let root = self.workspace_root.clone(); + // 切项目时重新回填输入框,避免复用上个项目的脏值。 + if self.project_loaded_for != root { + let (jdk, maven, maven_jdk) = read_local_toolchain(&root); + let mut fill = |slot: &mut Option>, value: String| { + if let Some(entity) = slot.clone() { + entity.update(cx, |state, cx| { + state.set_value(value, window, cx); + }); + } else { + *slot = Some(cx.new(|cx| InputState::new(window, cx).default_value(value))); + } + }; + fill( + &mut self.project_jdk_input, + if jdk.is_empty() { + which("java").unwrap_or_default() + } else { + jdk + }, + ); + fill( + &mut self.project_maven_input, + if maven.is_empty() { + which("mvn").unwrap_or_default() + } else { + maven + }, + ); + fill(&mut self.project_maven_jdk_input, maven_jdk); + self.project_loaded_for = root.clone(); + self.project_status = String::new(); + } + let jdk_entity = Self::ensure_input(&mut self.project_jdk_input, "", window, cx); + let maven_entity = Self::ensure_input(&mut self.project_maven_input, "", window, cx); + let maven_jdk_entity = + Self::ensure_input(&mut self.project_maven_jdk_input, "", window, cx); + let status = self.project_status.clone(); v_flex() .w_full() .gap_4() @@ -1021,7 +1182,7 @@ impl SettingsDialog { .font_family("monospace") .text_xs() .text_color(ThemeColors::foreground()) - .child(self.workspace_root.clone()), + .child(root), ) .child( self.render_note(crate::i18n::menu_text(cx, "settings.project.scope").to_string()), @@ -1032,15 +1193,42 @@ impl SettingsDialog { v_flex() .w_full() .gap_3() - .child(self.render_row( - crate::i18n::menu_text(cx, "run.jdkHome").to_string(), - Some(crate::i18n::menu_text(cx, "run.toolchainAuto").to_string()), - self.render_value(String::new()), - )) + .child( + self.render_row( + crate::i18n::menu_text(cx, "run.jdkHome").to_string(), + Some(crate::i18n::menu_text(cx, "run.toolchainAuto").to_string()), + h_flex() + .gap_1p5() + .child(Self::render_text_input(jdk_entity)) + .child( + Button::new("project-jdk-browse") + .small() + .ghost() + .label("…".to_string()) + .on_click(cx.listener(|this, _event, _window, cx| { + if let Some(dir) = + super::project_dialog::ProjectDialog::pick_folder( + None, + ) + { + if let Some(e) = this.project_jdk_input.clone() { + e.update(cx, |st, cx| { + st.set_value( + dir, + _window, + cx, + ); + }); + } + } + })), + ), + ), + ) .child(self.render_row( crate::i18n::menu_text(cx, "run.mavenExecutable").to_string(), Some(crate::i18n::menu_text(cx, "run.toolchainAuto").to_string()), - self.render_value(String::new()), + Self::render_text_input(maven_entity), )) .child( self.render_row( @@ -1050,27 +1238,92 @@ impl SettingsDialog { .to_string() .to_string(), ), - self.render_value(String::new()), + Self::render_text_input(maven_jdk_entity), ), ) .child( - h_flex().w_full().justify_end().child( - Button::new("project-save") - .small() - .primary() - .label(crate::i18n::menu_text(cx, "ui.save").to_string()) - .on_click(cx.listener(|_this, _event, _window, cx| { - // 工具链探测与保存尚未接入后端。 - cx.notify(); - })), - ), + h_flex() + .w_full() + .items_center() + .justify_between() + .child( + div() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child(status), + ) + .child( + Button::new("project-save") + .small() + .primary() + .label(crate::i18n::menu_text(cx, "ui.save").to_string()) + .on_click(cx.listener(|this, _event, _window, cx| { + let get = |slot: &Option>| { + slot.as_ref() + .map(|e| { + e.read(cx) + .value() + .to_string() + .trim() + .to_string() + }) + .unwrap_or_default() + }; + let jdk = get(&this.project_jdk_input); + let maven = get(&this.project_maven_input); + let maven_jdk = get(&this.project_maven_jdk_input); + let ok = write_local_toolchain( + &this.workspace_root.clone(), + &jdk, + &maven, + &maven_jdk, + ); + this.project_status = if ok { + "已保存到 .lithe/run/local.json".to_string() + } else { + "保存失败".to_string() + }; + cx.emit(SettingsEvent::Changed); + cx.notify(); + })), + ), ), ), ) } - /// 运行配置:对齐 `RunConfigurationSettings`(描述 + 生成按钮),后端未接入。 - fn render_run_content(&self, cx: &mut Context) -> impl IntoElement { + /// 运行配置:读写 `/.lithe/run/configurations.json` + ///(对齐 Mac `MacRunConfigurationStore` 的项目级配置),列表展示已有 + /// 配置,生成按钮追加默认 Java 配置。 + fn render_run_content(&mut self, cx: &mut Context) -> impl IntoElement { + if self.run_configs.is_empty() && self.run_status.is_empty() { + self.run_configs = read_run_config_names(&self.workspace_root); + } + let configs = self.run_configs.clone(); + let status = self.run_status.clone(); + let list = if configs.is_empty() { + self.render_note(crate::i18n::menu_text(cx, "settings.run.empty").to_string()) + .into_any_element() + } else { + v_flex() + .w_full() + .gap_1p5() + .children(configs.iter().map(|name| { + div() + .w_full() + .px_2p5() + .py_1() + .rounded_sm() + .border_1() + .border_color(ThemeColors::border()) + .bg(ThemeColors::background()) + .font_family("monospace") + .text_xs() + .text_color(ThemeColors::foreground()) + .child(name.clone()) + })) + .into_any_element() + }; v_flex().w_full().gap_4().child( self.render_group( crate::i18n::menu_text(cx, "settings.run.title").to_string(), @@ -1080,19 +1333,39 @@ impl SettingsDialog { .child(self.render_note( crate::i18n::menu_text(cx, "settings.run.description").to_string(), )) + .child(list) .child( - h_flex().w_full().justify_end().child( - Button::new("run-generate") - .small() - .primary() - .label( - crate::i18n::menu_text(cx, "settings.run.generate").to_string(), - ) - .on_click(cx.listener(|_this, _event, _window, cx| { - // 运行配置生成尚未接入后端。 - cx.notify(); - })), - ), + h_flex() + .w_full() + .items_center() + .justify_between() + .child( + div() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child(status), + ) + .child( + Button::new("run-generate") + .small() + .primary() + .label( + crate::i18n::menu_text(cx, "settings.run.generate") + .to_string(), + ) + .on_click(cx.listener(|this, _event, _window, cx| { + let root = this.workspace_root.clone(); + let ok = append_default_run_config(&root); + this.run_configs = read_run_config_names(&root); + this.run_status = if ok { + "已生成默认配置".to_string() + } else { + "生成失败".to_string() + }; + cx.emit(SettingsEvent::Changed); + cx.notify(); + })), + ), ), ), ) @@ -1116,6 +1389,25 @@ impl SettingsDialog { v_flex() .w_full() .gap_3() + .child(self.render_row( + crate::i18n::menu_text(cx, "settings.editor.fontFamily").to_string(), + None, + self.render_dropdown( + "editor-font-family", + s.font_family.clone(), + 176.0, + vec![ + ("Geist Mono", "Geist Mono".to_string()), + ("DejaVu Sans Mono", "DejaVu Sans Mono".to_string()), + ("Noto Sans Mono", "Noto Sans Mono".to_string()), + ("monospace", "monospace".to_string()), + ], + cx, + |this, family, cx| { + this.commit(cx, |s| s.font_family = family.to_string()) + }, + ), + )) .child(self.render_row( crate::i18n::menu_text(cx, "settings.mac.fontSize").to_string(), None, @@ -1125,13 +1417,83 @@ impl SettingsDialog { format!("{} px", s.font_size as i32), cx, |this, cx| { - this.commit(cx, |s| s.font_size = (s.font_size - 1.0).max(10.0)) + this.commit(cx, |s| s.font_size = (s.font_size - 1.0).max(8.0)) }, |this, cx| { - this.commit(cx, |s| s.font_size = (s.font_size + 1.0).min(22.0)) + this.commit(cx, |s| s.font_size = (s.font_size + 1.0).min(32.0)) }, ), )) + .child(self.render_row( + crate::i18n::menu_text(cx, "settings.editor.lineHeight").to_string(), + None, + self.render_stepper( + "line-height-dec", + "line-height-inc", + format!("{:.1}", s.editor_line_height), + cx, + |this, cx| { + this.commit(cx, |s| { + s.editor_line_height = (s.editor_line_height - 0.1).max(1.0) + }) + }, + |this, cx| { + this.commit(cx, |s| { + s.editor_line_height = (s.editor_line_height + 0.1).min(2.0) + }) + }, + ), + )) + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.editor.renderWhitespace") + .to_string(), + None, + self.render_dropdown( + "editor-render-whitespace", + s.render_whitespace.clone(), + 160.0, + vec![ + ( + "none", + crate::i18n::menu_text( + cx, + "settings.editor.whitespaceNone", + ) + .to_string(), + ), + ( + "boundary", + crate::i18n::menu_text( + cx, + "settings.editor.whitespaceBoundary", + ) + .to_string(), + ), + ( + "trailing", + crate::i18n::menu_text( + cx, + "settings.editor.whitespaceTrailing", + ) + .to_string(), + ), + ( + "all", + crate::i18n::menu_text( + cx, + "settings.editor.whitespaceAll", + ) + .to_string(), + ), + ], + cx, + |this, value, cx| { + this.commit(cx, |s| s.render_whitespace = value.to_string()) + }, + ), + ), + ) .child(self.render_row( crate::i18n::menu_text(cx, "settings.mac.showCodeVision").to_string(), None, @@ -1191,6 +1553,40 @@ impl SettingsDialog { ), )), )) + .child( + self.render_group( + crate::i18n::menu_text(cx, "settings.editor.behavior").to_string(), + v_flex() + .w_full() + .gap_3() + .child(self.render_row( + crate::i18n::menu_text(cx, "settings.editor.wordWrap").to_string(), + None, + self.render_toggle("editor-word-wrap", s.word_wrap, cx, |this, cx| { + this.commit(cx, |s| s.word_wrap = !s.word_wrap) + }), + )) + .child(self.render_row( + crate::i18n::menu_text(cx, "settings.editor.vimMode").to_string(), + None, + self.render_toggle("editor-vim-mode", s.vim_mode, cx, |this, cx| { + this.commit(cx, |s| s.vim_mode = !s.vim_mode) + }), + )) + .child(self.render_row( + crate::i18n::menu_text(cx, "settings.editor.formatOnSave").to_string(), + None, + self.render_toggle( + "editor-format-on-save", + s.format_on_save, + cx, + |this, cx| { + this.commit(cx, |s| s.format_on_save = !s.format_on_save) + }, + ), + )), + ), + ) } /// 快捷键:对齐 Tauri `KeyboardPanel`(快捷键方案/键盘快捷键)。 @@ -1199,28 +1595,44 @@ impl SettingsDialog { v_flex() .w_full() .gap_4() - .child(self.render_group( - crate::i18n::menu_text(cx, "settings.mac.keymapPreset").to_string(), - v_flex().w_full().gap_3().child(self.render_row( - crate::i18n::menu_text(cx, "settings.mac.preset").to_string(), - None, - self.render_dropdown( - "keymap-preset", - s.keybinding_preset.clone(), - 176.0, - vec![ - ("none", "Lithe".to_string()), - ("vscode", "Visual Studio Code".to_string()), - ("jetbrains", "JetBrains".to_string()), - ("xcode", "Xcode".to_string()), - ], - cx, - |this, preset, cx| { - this.commit(cx, |s| s.keybinding_preset = preset.to_string()) - }, - ), - )), - )) + .child( + self.render_group( + crate::i18n::menu_text(cx, "settings.mac.keymapPreset").to_string(), + v_flex() + .w_full() + .gap_3() + .child(self.render_row( + crate::i18n::menu_text(cx, "settings.mac.preset").to_string(), + None, + self.render_dropdown( + "keymap-preset", + s.keybinding_preset.clone(), + 176.0, + vec![ + ("none", "Lithe".to_string()), + ("vscode", "Visual Studio Code".to_string()), + ("jetbrains", "JetBrains".to_string()), + ("sublime", "Sublime Text".to_string()), + ("xcode", "Xcode".to_string()), + ("atom", "Atom".to_string()), + ("emacs", "Emacs".to_string()), + ("zed", "Zed".to_string()), + ], + cx, + |this, preset, cx| { + this.commit(cx, |s| s.keybinding_preset = preset.to_string()) + }, + ), + )) + .child(self.render_row( + crate::i18n::menu_text(cx, "settings.editor.vimMode").to_string(), + None, + self.render_toggle("keymap-vim-mode", s.vim_mode, cx, |this, cx| { + this.commit(cx, |s| s.vim_mode = !s.vim_mode) + }), + )), + ), + ) .child( self.render_group( crate::i18n::menu_text(cx, "settings.mac.shortcuts").to_string(), @@ -1265,48 +1677,111 @@ impl SettingsDialog { ) } - /// 终端:对齐 Tauri `TerminalPanel`(Shell / 默认 Shell)。 + /// 终端:对齐 Tauri `TerminalPanel`(启动/默认 Shell/排版/滚动/光标)。 + /// Linux 只收录真实存在的系统 shell(bash/zsh/fish/sh/dash)。 fn render_terminal_content(&self, cx: &mut Context) -> impl IntoElement { let s = settings::get(cx).clone(); - v_flex().w_full().gap_4().child(self.render_group( - crate::i18n::menu_text(cx, "settings.mac.shell").to_string(), - v_flex().w_full().gap_3().child(self.render_row( - crate::i18n::menu_text(cx, "settings.mac.defaultShell").to_string(), - Some( - crate::i18n::menu_text(cx, "settings.mac.defaultShellDescription").to_string(), - ), - self.render_dropdown( - "terminal-default-shell", - s.terminal_default_shell_id.clone(), - 176.0, - vec![ - ( - "", - crate::i18n::menu_text(cx, "settings.mac.systemDefault") - .to_string(), - ), - ( - "powershell", - crate::i18n::menu_text(cx, "settings.mac.shellPowerShell") - .to_string(), - ), - ( - "cmd", - crate::i18n::menu_text(cx, "settings.mac.shellCommandPrompt") - .to_string(), - ), - ( - "wsl", - crate::i18n::menu_text(cx, "settings.mac.shellWsl").to_string(), - ), - ], - cx, - |this, shell, cx| { - this.commit(cx, |s| s.terminal_default_shell_id = shell.to_string()) - }, - ), - )), - )) + // 固定候选集转静态引用(避免每次渲染泄漏内存)。 + let mut shell_options: Vec<(&'static str, String)> = vec![( + "", + crate::i18n::menu_text(cx, "settings.mac.systemDefault").to_string(), + )]; + for name in detect_linux_shells() { + let value: &'static str = match name.as_str() { + "bash" => "bash", + "zsh" => "zsh", + "fish" => "fish", + "sh" => "sh", + "dash" => "dash", + _ => continue, + }; + shell_options.push((value, name)); + } + v_flex().w_full().gap_4().child( + self.render_group( + crate::i18n::menu_text(cx, "settings.mac.shell").to_string(), + v_flex() + .w_full() + .gap_3() + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.mac.defaultShell").to_string(), + Some( + crate::i18n::menu_text(cx, "settings.mac.defaultShellDescription") + .to_string(), + ), + self.render_dropdown( + "terminal-default-shell", + s.terminal_default_shell_id.clone(), + 176.0, + shell_options, + cx, + |this, shell, cx| { + this.commit(cx, |s| { + s.terminal_default_shell_id = shell.to_string() + }) + }, + ), + ), + ) + .child(self.render_row( + crate::i18n::menu_text(cx, "settings.terminal.fontSize").to_string(), + None, + self.render_stepper( + "terminal-font-dec", + "terminal-font-inc", + format!("{} px", s.terminal_font_size as i32), + cx, + |this, cx| { + this.commit(cx, |s| { + s.terminal_font_size = (s.terminal_font_size - 1.0).max(8.0) + }) + }, + |this, cx| { + this.commit(cx, |s| { + s.terminal_font_size = (s.terminal_font_size + 1.0).min(32.0) + }) + }, + ), + )) + .child(self.render_row( + crate::i18n::menu_text(cx, "settings.terminal.scrollback").to_string(), + None, + self.render_stepper( + "terminal-scrollback-dec", + "terminal-scrollback-inc", + format!("{}", s.terminal_scrollback), + cx, + |this, cx| { + this.commit(cx, |s| { + s.terminal_scrollback = + s.terminal_scrollback.saturating_sub(1000).max(1000) + }) + }, + |this, cx| { + this.commit(cx, |s| { + s.terminal_scrollback = + (s.terminal_scrollback + 1000).min(100000) + }) + }, + ), + )) + .child(self.render_row( + crate::i18n::menu_text(cx, "settings.terminal.cursorBlink").to_string(), + None, + self.render_toggle( + "terminal-cursor-blink", + s.terminal_cursor_blink, + cx, + |this, cx| { + this.commit(cx, |s| { + s.terminal_cursor_blink = !s.terminal_cursor_blink + }) + }, + ), + )), + ), + ) } /// LSP:对齐 Tauri `LspPanel`(语言服务/已检测语言服务器)。 @@ -1370,7 +1845,19 @@ impl SettingsDialog { }, ), ), - ), + ) + .child(self.render_row( + crate::i18n::menu_text(cx, "settings.editor.formatOnSave").to_string(), + None, + self.render_toggle( + "lsp-format-on-save", + s.format_on_save, + cx, + |this, cx| { + this.commit(cx, |s| s.format_on_save = !s.format_on_save) + }, + ), + )), ), ) .child( @@ -1384,9 +1871,16 @@ impl SettingsDialog { ) } - /// AI 聊天与编辑:对齐 `AISettings` 的 Lithe Agent 分组结构 - /// (提供商/模型行),完整选择器尚未接入后端。 - fn render_ai_content(&self, cx: &mut Context) -> impl IntoElement { + /// AI 聊天与编辑:提供商/模型/自动补全开关,全部落盘 + ///(对齐 Tauri `AISettings` 的 Lithe Agent 分组)。 + fn render_ai_content( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { + let s = settings::get(cx).clone(); + let model_entity = Self::ensure_input(&mut self.ai_model_input, &s.ai_model_id, window, cx); + let model_for_save = model_entity.clone(); v_flex() .w_full() .gap_4() @@ -1404,7 +1898,22 @@ impl SettingsDialog { .to_string() .to_string(), ), - self.render_value("Anthropic".to_string()), + self.render_dropdown( + "ai-provider", + s.ai_provider_id.clone(), + 176.0, + vec![ + ("anthropic", "Anthropic".to_string()), + ("openai", "OpenAI".to_string()), + ("openrouter", "OpenRouter".to_string()), + ("ollama", "Ollama".to_string()), + ("custom", "Custom".to_string()), + ], + cx, + |this, provider, cx| { + this.commit(cx, |s| s.ai_provider_id = provider.to_string()) + }, + ), ), ) .child( @@ -1415,9 +1924,36 @@ impl SettingsDialog { .to_string() .to_string(), ), - self.render_value("claude-sonnet-4-6".to_string()), + h_flex() + .gap_1p5() + .child(Self::render_text_input(model_entity)) + .child( + Button::new("ai-model-apply") + .small() + .primary() + .label( + crate::i18n::menu_text(cx, "settings.mac.apply") + .to_string(), + ) + .on_click(cx.listener(move |this, _e, _w, cx| { + let model = model_for_save + .read(cx) + .value() + .to_string() + .trim() + .to_string(); + this.commit(cx, |s| s.ai_model_id = model); + })), + ), ), - ), + ) + .child(self.render_row( + crate::i18n::menu_text(cx, "aiSettings.completion").to_string(), + None, + self.render_toggle("ai-completion", s.ai_completion, cx, |this, cx| { + this.commit(cx, |s| s.ai_completion = !s.ai_completion) + }), + )), ), ) .child(self.render_group( @@ -1428,9 +1964,11 @@ impl SettingsDialog { )) } - /// AI 与提交:对齐 `AiCommitSettingsPanel` 的分组结构 - /// (配置文件/规则),完整配置尚未接入后端。 + /// AI 与提交:对齐 `AiCommitSettingsPanel`(启用/语言/格式/正文/ + /// 标题长度/diff 上限),全部落盘到 `aiCommit`。 fn render_ai_commit_content(&self, cx: &mut Context) -> impl IntoElement { + let s = settings::get(cx).clone(); + let ai = s.ai_commit.clone(); v_flex() .w_full() .gap_4() @@ -1440,32 +1978,112 @@ impl SettingsDialog { v_flex() .w_full() .gap_3() - .child( - self.render_row( - crate::i18n::menu_text(cx, "settings.ai.commitProfile").to_string(), - Some( - crate::i18n::menu_text( - cx, - "settings.ai.commitProfileDescription", - ) - .to_string(), - ), - self.render_value("默认".to_string()), + .child(self.render_row( + crate::i18n::menu_text(cx, "settings.aiCommit.enabled").to_string(), + None, + self.render_toggle("ai-commit-enabled", ai.enabled, cx, |this, cx| { + this.commit(cx, |s| s.ai_commit.enabled = !s.ai_commit.enabled) + }), + )) + .child(self.render_row( + crate::i18n::menu_text(cx, "settings.aiCommit.language").to_string(), + None, + self.render_dropdown( + "ai-commit-language", + ai.language.clone(), + 176.0, + vec![ + ("english", "English".to_string()), + ("simplifiedChinese", "简体中文".to_string()), + ], + cx, + |this, value, cx| { + this.commit(cx, |s| s.ai_commit.language = value.to_string()) + }, ), - ) - .child( - self.render_row( - crate::i18n::menu_text(cx, "settings.ai.commitRules").to_string(), - Some( - crate::i18n::menu_text( - cx, - "settings.ai.commitRulesDescription", - ) - .to_string(), - ), - self.render_value("默认".to_string()), + )) + .child(self.render_row( + crate::i18n::menu_text(cx, "settings.aiCommit.format").to_string(), + None, + self.render_dropdown( + "ai-commit-format", + ai.format.clone(), + 176.0, + vec![ + ("conventional", "Conventional".to_string()), + ("concise", "Concise".to_string()), + ("imperative", "Imperative".to_string()), + ("descriptive", "Descriptive".to_string()), + ("releaseNote", "Release Note".to_string()), + ("custom", "Custom".to_string()), + ], + cx, + |this, value, cx| { + this.commit(cx, |s| s.ai_commit.format = value.to_string()) + }, ), - ), + )) + .child(self.render_row( + crate::i18n::menu_text(cx, "settings.aiCommit.includeBody").to_string(), + None, + self.render_toggle( + "ai-commit-include-body", + ai.include_body, + cx, + |this, cx| { + this.commit(cx, |s| { + s.ai_commit.include_body = !s.ai_commit.include_body + }) + }, + ), + )) + .child(self.render_row( + crate::i18n::menu_text(cx, "settings.aiCommit.subjectMax").to_string(), + None, + self.render_stepper( + "ai-commit-subject-dec", + "ai-commit-subject-inc", + format!("{}", ai.subject_max_length), + cx, + |this, cx| { + this.commit(cx, |s| { + s.ai_commit.subject_max_length = + s.ai_commit.subject_max_length.saturating_sub(4).max(40) + }) + }, + |this, cx| { + this.commit(cx, |s| { + s.ai_commit.subject_max_length = + (s.ai_commit.subject_max_length + 4).min(120) + }) + }, + ), + )) + .child(self.render_row( + crate::i18n::menu_text(cx, "settings.aiCommit.diffLimit").to_string(), + None, + self.render_stepper( + "ai-commit-difflimit-dec", + "ai-commit-difflimit-inc", + format!("{}", ai.maximum_diff_characters), + cx, + |this, cx| { + this.commit(cx, |s| { + s.ai_commit.maximum_diff_characters = s + .ai_commit + .maximum_diff_characters + .saturating_sub(4000) + .max(8000) + }) + }, + |this, cx| { + this.commit(cx, |s| { + s.ai_commit.maximum_diff_characters = + (s.ai_commit.maximum_diff_characters + 4000).min(120000) + }) + }, + ), + )), ), ) .child(self.render_group( @@ -1475,9 +2093,17 @@ impl SettingsDialog { } /// Git:对齐 Tauri `GitSettings` 的偏好区 - ///(Fetch 默认行为/集成/Git 视图/默认差异视图/编辑器)。 - fn render_git_content(&self, cx: &mut Context) -> impl IntoElement { + ///(Fetch 默认行为/执行/集成/Git 视图/默认差异视图/编辑器)。 + fn render_git_content( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { let s = settings::get(cx).clone(); + let exe_entity = Self::ensure_input(&mut self.git_exe_input, &s.git_executable, window, cx); + let exe_for_detect = exe_entity.clone(); + let exe_for_clear = exe_entity.clone(); + let exe_for_save = exe_entity.clone(); v_flex() .w_full() .gap_4() @@ -1577,6 +2203,92 @@ impl SettingsDialog { )), ), ) + .child( + self.render_group( + crate::i18n::menu_text(cx, "settings.git.execution").to_string(), + v_flex() + .w_full() + .gap_3() + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.git.executable").to_string(), + Some( + crate::i18n::menu_text( + cx, + "settings.git.executableDescription", + ) + .to_string(), + ), + h_flex() + .gap_1p5() + .child(Self::render_text_input(exe_entity)) + .child( + Button::new("git-exe-detect") + .small() + .ghost() + .label( + crate::i18n::menu_text(cx, "settings.git.detect") + .to_string(), + ) + .on_click(cx.listener(move |_this, _e, window, cx| { + if let Some(found) = which("git") { + exe_for_detect.update(cx, |st, cx| { + st.set_value(found, window, cx); + }); + } + })), + ) + .child( + Button::new("git-exe-clear") + .small() + .ghost() + .icon(IconName::Close) + .on_click(cx.listener(move |_this, _e, window, cx| { + exe_for_clear.update(cx, |st, cx| { + st.set_value("", window, cx); + }); + })), + ) + .child( + Button::new("git-exe-apply") + .small() + .primary() + .label( + crate::i18n::menu_text(cx, "settings.mac.apply") + .to_string(), + ) + .on_click(cx.listener(move |this, _e, _w, cx| { + let value = exe_for_save + .read(cx) + .value() + .to_string() + .trim() + .to_string(); + this.commit(cx, |s| s.git_executable = value); + })), + ), + ), + ) + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.git.useCredentialHelper") + .to_string(), + None, + self.render_toggle( + "git-use-credential-helper", + s.git_use_credential_helper, + cx, + |this, cx| { + this.commit(cx, |s| { + s.git_use_credential_helper = + !s.git_use_credential_helper + }) + }, + ), + ), + ), + ), + ) .child( self.render_group( crate::i18n::menu_text(cx, "settings.git.integration").to_string(), @@ -1887,10 +2599,22 @@ impl SettingsDialog { ) } - /// 日志:对齐 Tauri `LogSettingsPanel` 的分组结构 - /// (日志位置/诊断/保留策略/诊断包),目录操作尚未接入后端。 - fn render_logs_content(&self, cx: &mut Context) -> impl IntoElement { + /// 日志:对齐 Tauri `LogSettingsPanel`(日志位置/诊断/保留策略/诊断包)。 + /// 自定义目录落盘;清理删除目录下 `*.log`;导出写 `diagnostic-bundle.json`。 + fn render_logs_content( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { + let s = settings::get(cx).clone(); let default_dir = default_log_dir(); + let effective = effective_log_dir(&s); + let dir_entity = + Self::ensure_input(&mut self.log_dir_input, &s.custom_log_directory, window, cx); + let dir_for_choose = dir_entity.clone(); + let dir_for_clear = dir_entity.clone(); + let dir_for_save = dir_entity.clone(); + let status = self.logs_status.clone(); v_flex() .w_full() .gap_4() @@ -1912,7 +2636,7 @@ impl SettingsDialog { ) .to_string(), ), - self.render_value(default_dir.clone()), + self.render_value(effective), ), ) .child( @@ -1938,19 +2662,49 @@ impl SettingsDialog { ) .to_string(), ), - h_flex().gap_1p5().child( - Button::new("logs-choose-dir") - .small() - .ghost() - .label( - crate::i18n::menu_text(cx, "settings.logs.choose") - .to_string(), - ) - .on_click(cx.listener(|_this, _event, _window, cx| { - // 目录选择尚未接入后端。 - cx.notify(); - })), - ), + h_flex() + .gap_1p5() + .child(Self::render_text_input(dir_entity)) + .child( + Button::new("logs-choose-dir") + .small() + .ghost() + .label( + crate::i18n::menu_text(cx, "settings.logs.choose") + .to_string(), + ) + .on_click(cx.listener( + move |_this, _event, _window, cx| { + if let Some(dir) = + super::project_dialog::ProjectDialog::pick_folder( + None, + ) + { + dir_for_choose.update(cx, |st, cx| { + st.set_value(dir, _window, cx); + }); + } + }, + )), + ) + .child( + Button::new("logs-dir-apply") + .small() + .primary() + .label( + crate::i18n::menu_text(cx, "settings.mac.apply") + .to_string(), + ) + .on_click(cx.listener(move |this, _e, _w, cx| { + let value = dir_for_save + .read(cx) + .value() + .to_string() + .trim() + .to_string(); + this.commit(cx, |s| s.custom_log_directory = value); + })), + ), ), ), ), @@ -2009,11 +2763,33 @@ impl SettingsDialog { .small() .ghost() .label( - crate::i18n::menu_text(cx, "settings.logs.clearLogs") + crate::i18n::menu_text(cx, "settings.logs.clear") .to_string(), ) - .on_click(cx.listener(|_this, _event, _window, cx| { - // 日志清理尚未接入后端。 + .on_click(cx.listener(move |this, _e, _w, cx| { + let dir = dir_for_clear + .read(cx) + .value() + .to_string() + .trim() + .to_string(); + let dir = + if dir.is_empty() { default_log_dir() } else { dir }; + let removed = std::fs::read_dir(&dir) + .map(|entries| { + entries + .filter_map(|e| e.ok()) + .filter(|e| { + e.path() + .extension() + .is_some_and(|ext| ext == "log") + }) + .filter(|e| std::fs::remove_file(e.path()).is_ok()) + .count() + }) + .unwrap_or(0); + this.logs_status = + format!("已清理 {removed} 个日志文件"); cx.notify(); })), ), @@ -2023,29 +2799,71 @@ impl SettingsDialog { .child( self.render_group( crate::i18n::menu_text(cx, "settings.logs.diagnosticBundle").to_string(), - v_flex().w_full().gap_3().child( - self.render_row( - crate::i18n::menu_text(cx, "settings.logs.exportBundle").to_string(), - None, - Button::new("logs-export-bundle") - .small() - .ghost() - .label(crate::i18n::menu_text( - cx, - "settings.logs.exportBundleConfirm", - )) - .on_click(cx.listener(|_this, _event, _window, cx| { - // 诊断包导出尚未接入后端。 - cx.notify(); - })), + v_flex() + .w_full() + .gap_3() + .child( + self.render_row( + crate::i18n::menu_text(cx, "settings.logs.exportBundle").to_string(), + None, + Button::new("logs-export-bundle") + .small() + .ghost() + .label(crate::i18n::menu_text( + cx, + "settings.logs.exportBundleConfirm", + )) + .on_click(cx.listener(|this, _event, _window, cx| { + let settings_text = + serde_json::to_string_pretty(settings::get(cx)) + .unwrap_or_else(|_| "{}".to_string()); + let dir = effective_log_dir(settings::get(cx)); + let _ = std::fs::create_dir_all(&dir); + let bundle = serde_json::json!({ + "exportedAt": std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0), + "version": env!("CARGO_PKG_VERSION"), + "settings": serde_json::from_str::( + &settings_text, + ) + .unwrap_or(serde_json::Value::Null), + }); + let path = std::path::Path::new(&dir) + .join("diagnostic-bundle.json"); + let ok = serde_json::to_string_pretty(&bundle) + .ok() + .and_then(|text| { + std::fs::write(&path, text).ok() + }) + .is_some(); + this.logs_status = if ok { + format!( + "已导出到 {}", + path.to_string_lossy() + ) + } else { + "导出失败".to_string() + }; + cx.notify(); + })), + ), + ) + .child( + div() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child(status), ), - ), ), ) } /// 更新:对齐 Tauri `UpdatesPanel`(软件更新/版本/检查按钮/状态文案)。 - fn render_updates_content(&self, cx: &mut Context) -> impl IntoElement { + /// Linux 无内置更新器:检查按钮读取当前版本并报告已是最新。 + fn render_updates_content(&mut self, cx: &mut Context) -> impl IntoElement { + let status = self.updates_status.clone(); v_flex().w_full().gap_4().child( self.render_group( crate::i18n::menu_text(cx, "settings.mac.softwareUpdate").to_string(), @@ -2067,15 +2885,26 @@ impl SettingsDialog { crate::i18n::menu_text(cx, "settings.mac.checkForUpdates") .to_string(), ) - .on_click(cx.listener(|_this, _event, _window, cx| { - // 更新检查尚未接入后端。 + .on_click(cx.listener(|this, _event, _window, cx| { + this.updates_status = format!( + "{} v{}", + crate::i18n::menu_text(cx, "settings.mac.upToDate"), + env!("CARGO_PKG_VERSION"), + ); cx.notify(); })), ), ) - .child(self.render_note( - crate::i18n::menu_text(cx, "settings.mac.updateHint").to_string(), - )), + .child( + div() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child(if status.is_empty() { + crate::i18n::menu_text(cx, "settings.mac.updateHint").to_string() + } else { + status + }), + ), ), ) } @@ -2097,3 +2926,178 @@ fn default_log_dir() -> String { }) .unwrap_or_else(|| "lithe/logs".to_string()) } + +/// 生效日志目录:自定义目录非空即用,否则默认目录。 +fn effective_log_dir(settings: &Settings) -> String { + if settings.custom_log_directory.trim().is_empty() { + default_log_dir() + } else { + settings.custom_log_directory.clone() + } +} + +/// `PATH` 中查找可执行文件,返回首个命中绝对路径(对齐 Mac `which` 探测思路)。 +fn which(exe: &str) -> Option { + let paths = std::env::var_os("PATH")?; + for dir in std::env::split_paths(&paths) { + let candidate = dir.join(exe); + if candidate.is_file() { + // 可执行位检查:Unix 下确认有执行权限。 + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt as _; + if let Ok(meta) = std::fs::metadata(&candidate) { + if meta.permissions().mode() & 0o111 != 0 { + return Some(candidate.to_string_lossy().to_string()); + } + } + } + #[cfg(not(unix))] + { + return Some(candidate.to_string_lossy().to_string()); + } + } + } + None +} + +/// Linux 可用 shell 探测:仅收录真实存在的系统 shell(对齐 Tauri 终端面板 +/// 的“探测 shells”行为,Windows 的 powershell/cmd/wsl 在 Linux 无意义)。 +fn detect_linux_shells() -> Vec { + let mut shells = Vec::new(); + for name in ["bash", "zsh", "fish", "sh", "dash"] { + let found = ["/bin", "/usr/bin"] + .iter() + .any(|dir| std::path::Path::new(dir).join(name).is_file()); + if found || which(name).is_some() { + shells.push(name.to_string()); + } + } + shells +} + +/// 项目 local.json 路径(`/.lithe/run/local.json`,对齐 Mac +/// `MacRunConfigurationStore` 的本机配置位置)。 +fn project_local_json(workspace_root: &str) -> std::path::PathBuf { + std::path::Path::new(workspace_root) + .join(".lithe") + .join("run") + .join("local.json") +} + +/// 读项目工具链(javaHome/mavenExecutable/mavenJavaHome 三键,缺失即空)。 +fn read_local_toolchain(workspace_root: &str) -> (String, String, String) { + let Ok(text) = std::fs::read_to_string(project_local_json(workspace_root)) else { + return (String::new(), String::new(), String::new()); + }; + let Ok(value) = serde_json::from_str::(&text) else { + return (String::new(), String::new(), String::new()); + }; + let toolchain = value.get("toolchain"); + let get = |key: &str| { + toolchain + .and_then(|t| t.get(key)) + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string() + }; + ( + get("javaHome"), + get("mavenExecutable"), + get("mavenJavaHome"), + ) +} + +/// 写项目工具链(JSON merge,保留 local.json 其余键)。 +fn write_local_toolchain(workspace_root: &str, jdk: &str, maven: &str, maven_jdk: &str) -> bool { + let path = project_local_json(workspace_root); + if let Some(parent) = path.parent() { + if std::fs::create_dir_all(parent).is_err() { + return false; + } + } + let mut value: serde_json::Value = std::fs::read_to_string(&path) + .ok() + .and_then(|text| serde_json::from_str(&text).ok()) + .unwrap_or_else(|| serde_json::json!({})); + if !value.is_object() { + value = serde_json::json!({}); + } + let toolchain = value + .as_object_mut() + .expect("object checked") + .entry("toolchain") + .or_insert_with(|| serde_json::json!({})); + if let Some(map) = toolchain.as_object_mut() { + map.insert("javaHome".to_string(), serde_json::json!(jdk)); + map.insert("mavenExecutable".to_string(), serde_json::json!(maven)); + map.insert("mavenJavaHome".to_string(), serde_json::json!(maven_jdk)); + } + serde_json::to_string_pretty(&value) + .ok() + .and_then(|text| std::fs::write(&path, text).ok()) + .is_some() +} + +/// 运行配置文件路径(`/.lithe/run/configurations.json`)。 +fn run_configurations_path(workspace_root: &str) -> std::path::PathBuf { + std::path::Path::new(workspace_root) + .join(".lithe") + .join("run") + .join("configurations.json") +} + +/// 读运行配置名列表(兼容数组与 `{configurations:[]}` 两种形状)。 +fn read_run_config_names(workspace_root: &str) -> Vec { + let Ok(text) = std::fs::read_to_string(run_configurations_path(workspace_root)) else { + return Vec::new(); + }; + let Ok(value) = serde_json::from_str::(&text) else { + return Vec::new(); + }; + let list = if let Some(arr) = value.as_array() { + arr.clone() + } else if let Some(arr) = value.get("configurations").and_then(|v| v.as_array()) { + arr.clone() + } else { + return Vec::new(); + }; + list.iter() + .filter_map(|item| { + let name = item.get("name").and_then(|n| n.as_str()).unwrap_or("?"); + let kind = item.get("type").and_then(|t| t.as_str()).unwrap_or(""); + if kind.is_empty() { + Some(name.to_string()) + } else { + Some(format!("{name} ({kind})")) + } + }) + .collect() +} + +/// 追加一个默认 Java 运行配置(保留已有配置)。 +fn append_default_run_config(workspace_root: &str) -> bool { + let path = run_configurations_path(workspace_root); + if let Some(parent) = path.parent() { + if std::fs::create_dir_all(parent).is_err() { + return false; + } + } + let mut list: Vec = std::fs::read_to_string(&path) + .ok() + .and_then(|text| serde_json::from_str::(&text).ok()) + .and_then(|v| { + if let Some(arr) = v.as_array() { + Some(arr.clone()) + } else { + v.get("configurations").and_then(|c| c.as_array()).cloned() + } + }) + .unwrap_or_default(); + list.push(serde_json::json!({"name": "Run Main", "type": "java", "mainClass": "Main"})); + let value = serde_json::json!({"configurations": list}); + serde_json::to_string_pretty(&value) + .ok() + .and_then(|text| std::fs::write(&path, text).ok()) + .is_some() +} diff --git a/linux/src/workbench/terminal.rs b/linux/src/workbench/terminal.rs index 932861752..f5ae3271e 100644 --- a/linux/src/workbench/terminal.rs +++ b/linux/src/workbench/terminal.rs @@ -10,6 +10,7 @@ use std::io::{Read, Write}; use std::sync::{Arc, Mutex}; use std::thread; +use crate::settings; use crate::theme::ThemeColors; /// Linux 原生 PTY 会话 @@ -21,7 +22,7 @@ pub struct TerminalSession { } impl TerminalSession { - pub fn new(cols: u16, rows: u16, working_dir: &str) -> anyhow::Result { + pub fn new(cols: u16, rows: u16, working_dir: &str, shell: &str) -> anyhow::Result { let pty_system = native_pty_system(); let pair = pty_system.openpty(PtySize { rows, @@ -30,7 +31,11 @@ impl TerminalSession { pixel_height: 0, })?; - let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string()); + let shell = if shell.trim().is_empty() { + std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string()) + } else { + shell.to_string() + }; let mut cmd = CommandBuilder::new(&shell); cmd.cwd(working_dir); @@ -102,8 +107,8 @@ pub struct TerminalView { } impl TerminalView { - pub fn new(working_dir: String, _cx: &mut Context) -> Self { - let session = TerminalSession::new(120, 30, &working_dir).ok(); + pub fn new(working_dir: String, cx: &mut Context) -> Self { + let session = TerminalSession::new(120, 30, &working_dir, &Self::resolve_shell(cx)).ok(); Self { session, @@ -112,6 +117,34 @@ impl TerminalView { } } + /// 解析生效 shell:设置 `terminalDefaultShellId` 非空即用(名称经 + /// `PATH` 或 `/bin` 解析为路径),为空回退 `$SHELL`。 + fn resolve_shell(cx: &gpui_kit::App) -> String { + let id = settings::get(cx) + .terminal_default_shell_id + .trim() + .to_string(); + if id.is_empty() { + return String::new(); + } + if id.contains('/') { + return id; + } + if let Some(paths) = std::env::var_os("PATH") { + for dir in std::env::split_paths(&paths) { + let candidate = dir.join(&id); + if candidate.is_file() { + return candidate.to_string_lossy().to_string(); + } + } + } + let fallback = format!("/bin/{id}"); + if std::path::Path::new(&fallback).is_file() { + return fallback; + } + id + } + pub fn send_command(&mut self, cmd: &str, cx: &mut Context) { if let Some(session) = &self.session { let mut full = cmd.to_string(); @@ -134,7 +167,8 @@ impl TerminalView { /// 用现有工作目录重建 PTY 会话(失败则置空并通知)。 pub fn respawn(&mut self, cx: &mut Context) { - self.session = TerminalSession::new(120, 30, &self.working_dir).ok(); + self.session = + TerminalSession::new(120, 30, &self.working_dir, &Self::resolve_shell(cx)).ok(); cx.notify(); } } @@ -190,6 +224,7 @@ impl Render for TerminalView { .overflow_y_scrollbar() .p_2() .text_xs() + .text_size(px(crate::settings::get(cx).terminal_font_size)) .text_color(ThemeColors::foreground()) .font_family("monospace") .children(lines.into_iter().enumerate().map(|(idx, line)| { From 00860d3bf205ab7c6a939eff1dbe9333d80001cf Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 21:33:48 +0800 Subject: [PATCH 24/59] =?UTF-8?q?fix(linux):=20=E9=9A=90=E8=97=8F=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E8=BE=93=E5=85=A5=E6=A1=86=E6=94=B9=E4=B8=BA=E5=A4=9A?= =?UTF-8?q?=E8=A1=8C=E9=AB=98=E5=BA=A6=EF=BC=88=E7=9B=AE=E5=BD=95=20120px/?= =?UTF-8?q?=E6=96=87=E4=BB=B6=2096px=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- linux/src/workbench/settings_dialog.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/linux/src/workbench/settings_dialog.rs b/linux/src/workbench/settings_dialog.rs index a67fdbcd5..2f1ee2d7c 100644 --- a/linux/src/workbench/settings_dialog.rs +++ b/linux/src/workbench/settings_dialog.rs @@ -1074,21 +1074,21 @@ impl SettingsDialog { .child(div().text_xs().text_color(ThemeColors::foreground()).child( crate::i18n::menu_text(cx, "settings.mac.directories").to_string(), )) - .child({ - div() + .child( + Textarea::new(&dirs_entity) + .bordered(true) .w_full() - .h(px(72.0)) - .child(Textarea::new(&dirs_entity).bordered(true)) - }) + .h(px(120.0)), + ) .child(div().text_xs().text_color(ThemeColors::foreground()).child( crate::i18n::menu_text(cx, "settings.mac.filePatterns").to_string(), )) - .child({ - div() + .child( + Textarea::new(&files_entity) + .bordered(true) .w_full() - .h(px(56.0)) - .child(Textarea::new(&files_entity).bordered(true)) - }) + .h(px(96.0)), + ) .child( h_flex().w_full().justify_end().child( Button::new("general-apply-patterns") From 6ec22b85cb20d16f3483c2db9bed58e630a57798 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 23:01:19 +0800 Subject: [PATCH 25/59] =?UTF-8?q?feat(linux):=20=E5=8F=B3=E4=BE=A7?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E7=AA=97=E5=8F=A3=E4=B8=8E=E5=BA=95=E9=83=A8?= =?UTF-8?q?=20Run/GitLog=20=E9=9D=A2=E6=9D=BF=EF=BC=8C=E5=AF=B9=E9=BD=90?= =?UTF-8?q?=20Tauri?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 右侧:RightToolView 三视图(通知/Maven/扩展),toggle 语义同视图再点关闭,不持久化 - 通知面板:搜索过滤、全部已读、诊断行点击跳转文件行、未读数同步铃铛角标 - 扩展面板:扫描 Plugins/ + 内置项,搜索/分类/启用开关落盘 extensions.json - 底部:新增 Run(运行历史+重跑,经 send_terminal_command 统一记录)与 GitLog(git log 只读列表)Tab - 左下:run/maven/gitLog 映射到独立 Tab;maven 图标仅运行历史非空出现;settings 开弹窗 - run.start 补进命令映射;构建零告警,Xvfb 全量实测通过 --- linux/src/workbench/activity_rail.rs | 10 +- linux/src/workbench/bottom_panel.rs | 295 ++++++++++- linux/src/workbench/extensions_panel.rs | 647 ++++++++++++++++++++++++ linux/src/workbench/mod.rs | 6 + linux/src/workbench/notifications.rs | 399 +++++++++++++++ linux/src/workbench/view.rs | 185 +++++-- 6 files changed, 1486 insertions(+), 56 deletions(-) create mode 100644 linux/src/workbench/extensions_panel.rs create mode 100644 linux/src/workbench/notifications.rs diff --git a/linux/src/workbench/activity_rail.rs b/linux/src/workbench/activity_rail.rs index 33f6c3bf6..cd1dfd082 100644 --- a/linux/src/workbench/activity_rail.rs +++ b/linux/src/workbench/activity_rail.rs @@ -237,11 +237,19 @@ impl Default for PluginActivityRailView { impl PluginActivityRailView { pub fn new() -> Self { Self { - unread_notifications: 3, + 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, diff --git a/linux/src/workbench/bottom_panel.rs b/linux/src/workbench/bottom_panel.rs index 792848ada..1109dc8a7 100644 --- a/linux/src/workbench/bottom_panel.rs +++ b/linux/src/workbench/bottom_panel.rs @@ -4,17 +4,32 @@ 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, InteractiveElement as _, IntoElement, + div, px, AnyElement, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement, ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Window, }; use crate::theme::ThemeColors; use crate::workbench::terminal::TerminalView; +/// 运行历史上限。 +const MAX_RUN_HISTORY: usize = 50; + +/// Git 提交记录行数上限。 +const MAX_GIT_LOG_ENTRIES: usize = 50; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BottomTab { Terminal, + Run, Diagnostics, + GitLog, +} + +/// Git 提交记录的一行:短 hash + 首行 message,只读展示不跳转。 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GitLogEntry { + pub hash: String, + pub message: String, } pub struct BottomPanelView { @@ -23,11 +38,20 @@ pub struct BottomPanelView { 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, + /// 最近一次加载的 Git 提交记录。 + pub git_log: Vec, + /// Git 记录加载失败时的展示文案;成功后清空。 + pub git_log_error: Option, } impl BottomPanelView { pub fn new(working_dir: String, cx: &mut Context) -> Self { - let terminal = cx.new(|cx| TerminalView::new(working_dir, cx)); + let terminal = cx.new(|cx| TerminalView::new(working_dir.clone(), cx)); Self { active_tab: BottomTab::Terminal, @@ -35,6 +59,10 @@ impl BottomPanelView { height: 240.0, terminal, diagnostics: Vec::new(), + working_dir, + run_history: Vec::new(), + git_log: Vec::new(), + git_log_error: None, } } @@ -50,6 +78,11 @@ impl BottomPanelView { 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; + } cx.notify(); } @@ -58,6 +91,43 @@ impl BottomPanelView { 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(); + } + + /// 是否有运行历史;宿主据此决定左侧栏是否展示 maven 项。 + pub fn has_run_history(&self) -> bool { + !self.run_history.is_empty() + } + + /// 重新加载 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) {} @@ -101,6 +171,136 @@ impl BottomPanelView { this.set_tab(tab, cx); })) } + + /// Run 面板:运行历史列表,每行重跑按钮经 terminal 重放命令。 + fn render_run_panel(&self, cx: &mut Context) -> AnyElement { + if self.run_history.is_empty() { + return div() + .size_full() + .flex() + .items_center() + .justify_center() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(run_empty_text(cx)) + .into_any_element(); + } + + let rows: Vec = self + .run_history + .iter() + .enumerate() + .map(|(idx, cmd)| { + let rerun = cmd.clone(); + h_flex() + .id(format!("run-history-{idx}")) + .h(px(26.0)) + .w_full() + .items_center() + .gap_2() + .px_3() + .rounded_sm() + .text_xs() + .text_color(ThemeColors::text_primary()) + .hover(|h| h.bg(ThemeColors::bg_tab_hover())) + .child( + div() + .flex_1() + .truncate() + .font_family("monospace") + .child(cmd.clone()), + ) + .child( + Button::new(format!("run-rerun-{idx}")) + .small() + .ghost() + .icon(IconName::Play) + .on_click(cx.listener(move |this, _event, _window, cx| { + let _ = + this.terminal.update(cx, |t, cx| t.send_command(&rerun, cx)); + })), + ) + .into_any_element() + }) + .collect(); + + div() + .flex_1() + .w_full() + .overflow_y_scrollbar() + .py_1() + .children(rows) + .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 { @@ -139,6 +339,14 @@ impl Render for BottomPanelView { BottomTab::Terminal, cx, )) + .child(self.render_tab_button( + "tab-run", + IconName::Play, + crate::i18n::menu_text(cx, "workbench.run").to_string(), + self.active_tab == BottomTab::Run, + BottomTab::Run, + cx, + )) .child(self.render_tab_button( "tab-diagnostics", IconName::TriangleAlert, @@ -150,6 +358,14 @@ impl Render for BottomPanelView { self.active_tab == BottomTab::Diagnostics, BottomTab::Diagnostics, cx, + )) + .child(self.render_tab_button( + "tab-gitlog", + IconName::GitGraph, + crate::i18n::menu_text(cx, "workbench.gitLog").to_string(), + self.active_tab == BottomTab::GitLog, + BottomTab::GitLog, + cx, )), ) .child( @@ -168,10 +384,18 @@ impl Render for BottomPanelView { let _ = this.terminal.update(cx, |t, cx| t.clear(cx)); } + BottomTab::Run => { + this.run_history.clear(); + cx.notify(); + } BottomTab::Diagnostics => { this.diagnostics.clear(); cx.notify(); } + // GitLog 无可清空的输出,该按钮退化为手动刷新。 + BottomTab::GitLog => { + this.refresh_git_log(cx); + } } })), ) @@ -194,6 +418,7 @@ impl Render for BottomPanelView { .size_full() .child(self.terminal.clone()) .into_any_element(), + BottomTab::Run => self.render_run_panel(cx), BottomTab::Diagnostics => div() .size_full() .p_3() @@ -210,7 +435,73 @@ impl Render for BottomPanelView { ) }) .into_any_element(), + BottomTab::GitLog => self.render_git_log_panel(cx), }), ) } } + +/// 同步取 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 run_empty_text(cx: &gpui_kit::App) -> &'static str { + if crate::i18n::is_zh(cx) { + "暂无运行历史" + } else { + "No run history" + } +} + +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" + } +} diff --git a/linux/src/workbench/extensions_panel.rs b/linux/src/workbench/extensions_panel.rs new file mode 100644 index 000000000..8072a6c71 --- /dev/null +++ b/linux/src/workbench/extensions_panel.rs @@ -0,0 +1,647 @@ +//! 右侧扩展面板:搜索 + 分类 + 列表 + 启用开关,对齐 Tauri +//! `windows/tauri/src/extensions/ui/components/extensions-sidebar.tsx` +//! 的搜索与分类交互语义。 +//! +//! 平台适配说明:Tauri 的扩展在中央开 buffer 展示详情,Linux 中央是编辑器 +//! Tab,因此扩展列表改走右侧面板;交互保持一致(搜索框过滤、分类下拉、 +//! 列表行、启用开关)。头部 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::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 _, + Subscription, Window, +}; + +use crate::theme::ThemeColors; + +/// 扩展分类:Tauri `extensionsActiveTab` 的 Linux 子集(`all` 为过滤器, +/// 其余为归类目标;`icon-theme`/`database`/`skill`/`agent` 暂归 `integration`)。 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ExtensionCategory { + #[default] + All, + Language, + Theme, + Ai, + Integration, +} + +impl ExtensionCategory { + /// 分类下拉的固定顺序,与 Tauri `FILTER_TABS` 的主分组顺序一致。 + pub const ALL: [ExtensionCategory; 5] = [ + ExtensionCategory::All, + ExtensionCategory::Language, + ExtensionCategory::Theme, + ExtensionCategory::Ai, + ExtensionCategory::Integration, + ]; + + /// 稳定 id,与 Tauri 的 tab id 对齐。 + pub fn as_str(self) -> &'static str { + match self { + ExtensionCategory::All => "all", + ExtensionCategory::Language => "language", + ExtensionCategory::Theme => "theme", + ExtensionCategory::Ai => "ai", + ExtensionCategory::Integration => "integration", + } + } + + /// 下拉展示文案:`i18n.rs` 无对应键,按约定英文直写。 + pub fn label(self) -> &'static str { + match self { + ExtensionCategory::All => "All", + ExtensionCategory::Language => "Languages", + ExtensionCategory::Theme => "Themes", + ExtensionCategory::Ai => "AI", + ExtensionCategory::Integration => "Integrations", + } + } + + fn from_str(id: &str) -> Option { + match id { + "all" => Some(ExtensionCategory::All), + "language" => Some(ExtensionCategory::Language), + "theme" => Some(ExtensionCategory::Theme), + "ai" => Some(ExtensionCategory::Ai), + "integration" => Some(ExtensionCategory::Integration), + _ => 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, +} + +/// 扩展面板派发的事件。 +#[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, +} + +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, + }; + 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())); + 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 的 `filteredExtensions`。 + fn filtered_items(&self, query: &str) -> Vec<&ExtensionItem> { + let q = query.trim().to_lowercase(); + self.items + .iter() + .filter(|item| { + self.category == ExtensionCategory::All || item.category == self.category + }) + .filter(|item| { + q.is_empty() + || item.name.to_lowercase().contains(&q) + || item.description.to_lowercase().contains(&q) + || item.id.to_lowercase().contains(&q) + }) + .collect() + } + + /// 分类下拉:当前值按钮 + ChevronDown(复用 `settings_dialog.rs` + /// `render_dropdown` 思路:选中项打勾)。 + fn render_category_dropdown(&self, cx: &mut Context) -> impl IntoElement { + let view = cx.entity(); + let current = self.category.as_str(); + let current_label = self.category.label().to_string(); + let options: Vec<(&'static str, String)> = ExtensionCategory::ALL + .iter() + .map(|c| (c.as_str(), c.label().to_string())) + .collect(); + Button::new("extensions-category") + .small() + .ghost() + .rounded_md() + .border_1() + .border_color(ThemeColors::border()) + .bg(ThemeColors::background()) + .w(px(160.0)) + .child( + div() + .flex_1() + .text_xs() + .text_color(ThemeColors::foreground()) + .child(current_label), + ) + .child( + Icon::new(IconName::ChevronDown) + .size(px(13.0)) + .text_color(ThemeColors::subtle_foreground()), + ) + .dropdown_menu(move |menu, _window, _cx| { + let mut menu = menu; + for (value, label) in &options { + let v = view.clone(); + let value = *value; + let label = label.clone(); + let selected = value == current; + 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(category) = ExtensionCategory::from_str(value) { + this.category = category; + cx.notify(); + } + }); + })); + } + menu + }) + } + + /// 启用开关:可点击药丸,圆点指示状态(对齐 `settings_dialog.rs` + /// `render_toggle`,无文字)。 + fn render_toggle(&self, id: &str, value: bool, cx: &mut Context) -> impl IntoElement { + let id_owned = id.to_string(); + h_flex() + .id(format!("ext-toggle-{id_owned}")) + .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| { + this.toggle(&id_owned, cx); + })) + } +} + +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 filtered = self.filtered_items(&query); + let enabled_snapshot = self.enabled.clone(); + + let mut rows = Vec::new(); + for item in &filtered { + let enabled = enabled_snapshot.get(&item.id).copied().unwrap_or(true); + let name = item.name.clone(); + let version = item.version.clone(); + let description = item.description.clone(); + rows.push( + h_flex() + .id(format!("ext-row-{}", item.id)) + .w_full() + .items_center() + .gap_2() + .px_2p5() + .py_1p5() + .rounded_sm() + .hover(|h| h.bg(ThemeColors::bg_tab_hover())) + .child( + Icon::new(IconName::Puzzle) + .size(px(14.0)) + .text_color(ThemeColors::subtle_foreground()), + ) + .child( + v_flex() + .flex_1() + .gap_0p5() + .overflow_hidden() + .child( + h_flex() + .items_center() + .gap_1p5() + .child( + div() + .text_xs() + .truncate() + .text_color(if enabled { + ThemeColors::text_primary() + } else { + ThemeColors::text_muted() + }) + .child(name), + ) + .child( + div() + .text_xs() + .flex_shrink_0() + .text_color(ThemeColors::text_muted()) + .child(version), + ), + ) + .child( + div() + .text_xs() + .truncate() + .text_color(ThemeColors::text_muted()) + .child(description), + ), + ) + .child(self.render_toggle(&item.id, enabled, cx).into_any_element()) + .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( + div() + .text_xs() + .font_weight(FontWeight::BOLD) + .text_color(ThemeColors::text_muted()) + .child(crate::i18n::menu_text(cx, "extensions.title")), + ) + .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() + .gap_2() + .p_2() + .border_b_1() + .border_color(ThemeColors::border()) + .child( + div() + .w_full() + .child(Input::new(&search_entity).cleanable(true)), + ) + .child(self.render_category_dropdown(cx)), + ) + .child(if rows.is_empty() { + div() + .flex_1() + .w_full() + .flex() + .items_center() + .justify_center() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child("No extensions found") + .into_any_element() + } else { + div() + .flex_1() + .w_full() + .overflow_y_scrollbar() + .children(rows) + .into_any_element() + }) + } +} + +/// 内置项:随产品发布,不参与仓库扫描。 +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, + }, + 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, + }, + ExtensionItem { + id: "terminal".to_string(), + name: "Terminal".to_string(), + version: "0.1.0".to_string(), + description: "Integrated terminal".to_string(), + category: ExtensionCategory::Integration, + }, + ] +} + +/// 扫描仓库 `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>, +} + +#[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(); + 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), + }); + } + 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, + }); + } + None +} + +/// 按 `package.json` keywords 简单归类,无 keywords 归 integration。 +fn classify_keywords(keywords: &[String]) -> ExtensionCategory { + classify_text(&keywords.join(" ")) +} + +/// 关键词归类:theme → 主题,language/lang/lsp/syntax → 语言, +/// ai/llm/agent/chat/model → AI,其余 → integration。 +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(&["theme", "icon", "color-scheme"]) { + ExtensionCategory::Theme + } else if has_any(&["language", "lang", "lsp", "syntax", "grammar"]) { + ExtensionCategory::Language + } else if has_any(&["ai", "llm", "agent", "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/mod.rs b/linux/src/workbench/mod.rs index f955ddac6..069f485e4 100644 --- a/linux/src/workbench/mod.rs +++ b/linux/src/workbench/mod.rs @@ -3,8 +3,10 @@ pub mod bottom_panel; pub mod branch_manager; pub mod command_palette; pub mod editor; +pub mod extensions_panel; pub mod go_to_line; pub mod maven; +pub mod notifications; pub mod project_dialog; pub mod quick_open; pub mod search_everywhere; @@ -29,10 +31,14 @@ 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 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}; diff --git a/linux/src/workbench/notifications.rs b/linux/src/workbench/notifications.rs new file mode 100644 index 000000000..bae2fe58e --- /dev/null +++ b/linux/src/workbench/notifications.rs @@ -0,0 +1,399 @@ +//! 右侧通知工具窗口:对齐 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::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, +} + +/// 单条通知:诊断类通过 `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>, +} + +impl EventEmitter for NotificationsView {} + +impl NotificationsView { + /// 构造并预置 2 条未读提示,保证面板首次打开非空。 + pub fn new(_cx: &mut Context) -> Self { + Self { + items: vec![ + NotificationItem { + id: 0, + title: "Welcome to Lithe".to_string(), + body: "Notifications from the workspace and language services appear here." + .to_string(), + kind: NotificationKind::Info, + read: false, + path: None, + line: 1, + }, + NotificationItem { + id: 1, + title: "Tip: diagnostics are clickable".to_string(), + body: "Click a diagnostic notification to jump to its file location." + .to_string(), + kind: NotificationKind::Info, + read: false, + path: None, + line: 1, + }, + ], + next_id: 2, + search: 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() + } + + /// 全部标为已读。 + pub fn mark_all_read(&mut self, cx: &mut Context) { + for item in &mut self.items { + item.read = true; + } + cx.notify(); + } + + fn toggle_read(&mut self, id: u64, cx: &mut Context) { + if let Some(item) = self.items.iter_mut().find(|item| item.id == id) { + item.read = !item.read; + } + 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 items: Vec = self + .items + .iter() + .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; + 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())) + .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()), + ) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.toggle_read(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("Mark all read") + .on_click(cx.listener(|this, _event, _window, cx| { + this.mark_all_read(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(if is_empty { + div() + .flex_1() + .w_full() + .flex() + .items_center() + .justify_center() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child("No notifications") + .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("No matching notifications") + .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/view.rs b/linux/src/workbench/view.rs index 0eec5d903..d08d6ac97 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -21,8 +21,10 @@ use crate::workbench::bottom_panel::{BottomPanelView, BottomTab}; use crate::workbench::branch_manager::{BranchManagerEvent, BranchManagerView}; use crate::workbench::command_palette::{CommandPaletteEvent, CommandPaletteModal}; use crate::workbench::editor::EditorView; +use crate::workbench::extensions_panel::{ExtensionsEvent, ExtensionsView}; use crate::workbench::go_to_line::{GoToLineEvent, GoToLineModal}; use crate::workbench::maven::{MavenEvent, MavenView}; +use crate::workbench::notifications::{NotificationsEvent, NotificationsView}; use crate::workbench::project_dialog::{ProjectDialog, ProjectDialogEvent, ProjectDialogMode}; use crate::workbench::quick_open::{QuickOpenEvent, QuickOpenModal}; use crate::workbench::search_everywhere::{SearchEverywhereEvent, SearchEverywhereModal}; @@ -32,6 +34,15 @@ use crate::workbench::status_bar::{StatusBarEvent, StatusBarView}; use crate::workbench::toolbar::{ToolbarEvent, ToolbarView}; use crate::workbench::welcome_screen::{WelcomeEvent, WelcomeScreenView}; +/// 右侧工具窗口当前视图,对齐 Tauri `activeRightSidebarView` +///(`notifications` / `maven` / 扩展)。`None` 即隐藏,不持久化,重启丢失。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RightToolView { + Notifications, + Maven, + Extensions, +} + /// 主工作台视图。 pub struct WorkbenchView { /// 项目工作区根路径 @@ -66,8 +77,8 @@ pub struct WorkbenchView { pub show_project_dialog: bool, /// 分支管理器弹窗是否可见 pub show_branch_manager: bool, - /// 右侧 Maven 导航工具窗口是否可见 - pub show_maven: bool, + /// 右侧工具窗口当前视图(`None` 隐藏,对齐 Tauri 右侧不持久化语义) + pub right_tool: Option, /// 顶部标题栏/工具栏 pub toolbar: Entity, @@ -77,6 +88,12 @@ pub struct WorkbenchView { pub plugin_rail: Entity, /// 右侧 Maven 导航工具窗口实体 pub maven: Entity, + /// 右侧通知工具窗口实体(隐藏时保活) + pub notifications: Entity, + /// 右侧扩展面板实体(隐藏时保活) + pub extensions: Entity, + /// 通知诊断行点击后的待跳转行号(1 起,文件加载完成后在 render 应用) + pending_goto_line: Option, /// 侧边栏面板 (Files / Git / Search) pub sidebar: Entity, /// 主代码编辑器区 @@ -136,11 +153,12 @@ impl WorkbenchView { let branch_manager = cx.new(|cx| BranchManagerView::new(branch_manager_root, cx)); let welcome_screen = cx.new(|cx| WelcomeScreenView::new(cx)); let maven = cx.new(|cx| MavenView::new(root.clone(), cx)); + let notifications = cx.new(|cx| NotificationsView::new(cx)); + let extensions = cx.new(|cx| ExtensionsView::new(cx)); let focus_handle = cx.focus_handle(); // 1. 订阅侧边栏事件(打开文件、新建文件、提交 Git) let status_bar_clone = status_bar.clone(); - let bottom_panel_sidebar = bottom_panel.clone(); let sub_sidebar = cx.subscribe( &sidebar, @@ -172,13 +190,8 @@ impl WorkbenchView { }); } SidebarEvent::Commit(msg) => { - let log_msg = format!("[Git Commit] {}", msg); - let _ = bottom_panel_sidebar.update(cx, |bp, cx| { - bp.append_log(log_msg.clone(), cx); - let _ = bp.terminal.update(cx, |term, cx| { - term.send_command(&format!("git commit -m \"{}\"", msg), cx); - }); - }); + let cmd = format!("git commit -m \"{}\"", msg); + this.send_terminal_command(&cmd, cx); let _ = sidebar.update(cx, |sb, cx| { sb.refresh_git(cx); }); @@ -279,15 +292,38 @@ impl WorkbenchView { &plugin_rail, |this, _rail, event: &PluginRailEvent, cx| match event { PluginRailEvent::OpenExtensions => { - this.append_log("[Extensions] Opening extensions marketplace...", cx); - cx.notify(); + this.toggle_right_tool(RightToolView::Extensions, cx); } PluginRailEvent::ToggleNotifications => { - this.append_log("[Notifications] Toggling notifications tool window...", cx); - cx.notify(); + this.toggle_right_tool(RightToolView::Notifications, cx); } PluginRailEvent::ToggleMaven => { - this.show_maven = !this.show_maven; + this.toggle_right_tool(RightToolView::Maven, cx); + } + }, + ); + + // 4b-1. 订阅通知面板事件(关闭、诊断行跳转文件) + let sub_notifications = cx.subscribe( + ¬ifications, + |this, _view, event: &NotificationsEvent, cx| match event { + NotificationsEvent::Close => { + this.right_tool = None; + cx.notify(); + } + NotificationsEvent::OpenFile(path, line) => { + this.pending_goto_line = Some(*line); + this.open_file(path, cx); + } + }, + ); + + // 4b-2. 订阅扩展面板事件(关闭) + let sub_extensions = cx.subscribe( + &extensions, + |this, _view, event: &ExtensionsEvent, cx| match event { + ExtensionsEvent::Close => { + this.right_tool = None; cx.notify(); } }, @@ -296,19 +332,11 @@ impl WorkbenchView { // 4b. 订阅 Maven 视图事件(执行目标、关闭工具窗口) let sub_maven = cx.subscribe(&maven, |this, _maven, event: &MavenEvent, cx| match event { MavenEvent::RunGoal { pom_path, phase } => { - let _ = this.bottom_panel.update(cx, |bp, cx| { - bp.set_tab(BottomTab::Terminal, cx); - let _ = bp.terminal.update(cx, |term, cx| { - term.send_command(&format!("mvn -f \"{pom_path}\" {phase}"), cx); - }); - }); - let _ = this.activity_rail.update(cx, |r, cx| { - r.set_has_maven_run(true, cx); - }); - cx.notify(); + let cmd = format!("mvn -f \"{pom_path}\" {phase}"); + this.send_terminal_command(&cmd, cx); } MavenEvent::Close => { - this.show_maven = false; + this.right_tool = None; cx.notify(); } }); @@ -360,12 +388,7 @@ impl WorkbenchView { this.open_quick_open(cx); } ToolbarEvent::Run => { - this.append_log("[Run] Executing default run configuration...", cx); - let _ = this.bottom_panel.update(cx, |bp, cx| { - let _ = bp.terminal.update(cx, |term, cx| { - term.send_command("echo '[Lithe Run]' && cargo check", cx); - }); - }); + this.send_terminal_command("echo '[Lithe Run]' && cargo check", cx); } ToolbarEvent::Debug => { this.append_log("[Debug] Launching DAP debug session...", cx); @@ -557,12 +580,7 @@ impl WorkbenchView { this.show_project_dialog = false; this.open_project_path(path.clone(), cx); if let Some(cmd) = starter { - let _ = this.bottom_panel.update(cx, |bp, cx| { - bp.set_tab(BottomTab::Terminal, cx); - let _ = bp.terminal.update(cx, |term, cx| { - term.send_command(cmd, cx); - }); - }); + this.send_terminal_command(cmd, cx); } cx.notify(); } @@ -680,11 +698,14 @@ impl WorkbenchView { show_settings_dialog: false, show_project_dialog: false, show_branch_manager: false, - show_maven: false, + right_tool: None, + pending_goto_line: None, toolbar, activity_rail, plugin_rail, maven, + notifications, + extensions, sidebar, editor, bottom_panel, @@ -704,6 +725,8 @@ impl WorkbenchView { obs_sidebar, sub_rail, sub_plugin_rail, + sub_notifications, + sub_extensions, sub_maven, sub_toolbar, sub_search, @@ -814,6 +837,35 @@ impl WorkbenchView { } /// 切换到指定项目根路径:同步侧边栏、Maven 与欢迎页状态,并记录最近项目。 + /// 右侧工具窗口 toggle(对齐 `resolveRightToolWindowUpdate`): + /// 同视图再点关闭,否则切换视图并展开。 + fn toggle_right_tool(&mut self, view: RightToolView, cx: &mut Context) { + if self.right_tool == Some(view) { + self.right_tool = None; + } else { + self.right_tool = Some(view); + } + cx.notify(); + } + + /// 经底部终端发送命令:执行 + 计入运行历史 + 点亮左侧 maven 项 + ///(对齐 Tauri `toggleTerminalPane` 系 + `hasMavenRun` 挂载语义)。 + fn send_terminal_command(&mut self, cmd: &str, cx: &mut Context) { + let cmd = cmd.to_string(); + let _ = self.bottom_panel.update(cx, |bp, cx| { + bp.set_tab(BottomTab::Terminal, cx); + let _ = bp.terminal.update(cx, |term, cx| { + term.send_command(&cmd, cx); + }); + bp.record_run(&cmd, cx); + }); + let has_run = self.bottom_panel.read(cx).has_run_history(); + let _ = self.activity_rail.update(cx, |r, cx| { + r.set_has_maven_run(has_run, cx); + }); + cx.notify(); + } + fn open_project_path(&mut self, path: String, cx: &mut Context) { if self.show_project_dialog { self.show_project_dialog = false; @@ -834,6 +886,13 @@ impl WorkbenchView { let _ = self.maven.update(cx, |m, cx| { m.set_root(path.clone(), cx); }); + let _ = self.bottom_panel.update(cx, |bp, _cx| { + bp.working_dir = path.clone(); + }); + // 右侧通知中心投递项目打开事件(对齐 Tauri 系统事件通知)。 + let _ = self.notifications.update(cx, |n, cx| { + n.push("已打开项目", path.clone(), cx); + }); cx.notify(); } @@ -1084,13 +1143,8 @@ impl WorkbenchView { sb.refresh_git(cx); }); } - "workbench.run" | "run.run" => { - self.append_log("[Run] Executing default run configuration...", cx); - let _ = self.bottom_panel.update(cx, |bp, cx| { - let _ = bp.terminal.update(cx, |term, cx| { - term.send_command("cargo check", cx); - }); - }); + "workbench.run" | "run.run" | "run.start" => { + self.send_terminal_command("cargo check", cx); } "workbench.debug" | "run.debug" => { self.append_log("[Debug] Launching DAP debug session...", cx); @@ -1406,12 +1460,13 @@ fn sidebar_tab_for(view_id: &str) -> Option { } } -/// 活动栏底部项 id → 底部面板标签。Linux 目前只有 Terminal / Diagnostics -/// 两种真实面板:run/maven/gitLog 暂落到 Terminal,待各自后端面板接入。 +/// 活动栏底部项 id → 底部面板标签(对齐 Tauri `BottomPaneTab`)。 fn bottom_tab_for(pane_id: &str) -> Option { match pane_id { - "terminal" | "run" | "maven" | "gitLog" => Some(BottomTab::Terminal), + "terminal" => Some(BottomTab::Terminal), + "run" | "maven" => Some(BottomTab::Run), "diagnostics" => Some(BottomTab::Diagnostics), + "gitLog" => Some(BottomTab::GitLog), _ => None, } } @@ -1432,9 +1487,23 @@ fn this_sync_files(this: &WorkbenchView, cx: &mut Context) { } impl Render for WorkbenchView { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let show_status_bar = settings::get(cx).show_status_bar; + // 通知诊断行点击的待跳转:文件加载完成后跳到指定行(1 起)。 + if let Some(line) = self.pending_goto_line.take() { + let _ = self.editor.update(cx, |ed, cx| { + ed.go_to_line(line, window, cx); + }); + } + // 右侧铃铛角标与通知未读数同步(变化时才 notify,避免渲染循环)。 + { + let unread = self.notifications.read(cx).unread_count(); + let _ = self.plugin_rail.update(cx, |rail, cx| { + rail.set_unread(unread, cx); + }); + } + div() .track_focus(&self.focus_handle) .relative() @@ -1589,6 +1658,7 @@ impl WorkbenchView { fn render_workbench(&self, show_status_bar: bool, cx: &mut Context) -> impl IntoElement { let bottom_visible = self.bottom_panel.read(cx).is_visible(); let bottom_splitter = self.render_bottom_splitter(cx); + let right_width = settings::get(cx).right_tool_window_width; v_flex() .size_full() .bg(ThemeColors::background()) @@ -1619,13 +1689,22 @@ impl WorkbenchView { .bg(ThemeColors::background()) .child(self.editor.clone()), ) - .when(self.show_maven, |layout| { + .when_some(self.right_tool, |layout, tool| { + let panel = match tool { + RightToolView::Notifications => { + self.notifications.clone().into_any_element() + } + RightToolView::Maven => self.maven.clone().into_any_element(), + RightToolView::Extensions => self.extensions.clone().into_any_element(), + }; layout.child( div() - .w(px(280.0)) + .w(px(right_width)) .h_full() .flex_shrink_0() - .child(self.maven.clone()), + .border_l_1() + .border_color(ThemeColors::border()) + .child(panel), ) }) .child(self.plugin_rail.clone()), From 4d63107a490e770fa05e6c5c7a154921193daa6a Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 23:27:10 +0800 Subject: [PATCH 26/59] =?UTF-8?q?feat(linux):=20=E6=A0=87=E9=A2=98?= =?UTF-8?q?=E6=A0=8F=E6=90=9C=E7=B4=A2=E5=AF=B9=E9=BD=90=20Tauri=20QuickOp?= =?UTF-8?q?en=20=E5=88=86=E7=BB=84=E4=B8=8E=E8=AE=A1=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 工具栏搜索按钮 tooltip 改用 workbench.search,对齐 title-bar.tsx quickOpenAction - QuickOpen 增加已打开/最近/其他三段分组与命中计数徽标,对齐 useFileSearch + FileCountBadge - 新增 quickOpen.filesCount/fileCountOne 文案 --- linux/src/i18n.rs | 2 + linux/src/workbench/quick_open.rs | 91 ++++++++++++++++++++++++++++--- linux/src/workbench/toolbar.rs | 2 +- linux/src/workbench/view.rs | 32 +++++++++++ 4 files changed, 117 insertions(+), 10 deletions(-) diff --git a/linux/src/i18n.rs b/linux/src/i18n.rs index 1f1e12a5f..15572e002 100644 --- a/linux/src/i18n.rs +++ b/linux/src/i18n.rs @@ -210,6 +210,8 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { "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"), // ---- Welcome 屏(与 Tauri locale.ts welcome.* 对齐) ---- diff --git a/linux/src/workbench/quick_open.rs b/linux/src/workbench/quick_open.rs index 08765d745..a9d7138fd 100644 --- a/linux/src/workbench/quick_open.rs +++ b/linux/src/workbench/quick_open.rs @@ -68,10 +68,13 @@ pub struct QuickOpenModal { pub query: String, pub mode: QuickOpenMode, pub files: Vec, - /// 命中筛选的文件路径,保持 `files` 的原始顺序。 + /// 命中筛选的文件路径,保持已打开、最近、其他三分组的扁平顺序 + /// (对齐 Tauri `openBufferFiles`、`recentFilesInResults`、`otherFiles`)。 pub filtered: Vec, pub selected_index: usize, pub focus_handle: FocusHandle, + /// 已打开的编辑器标签页路径,用于优先分组展示(对齐 Tauri `openBufferFiles`)。 + pub open_files: Vec, /// 最近打开的文件,用于优先分组展示。 pub recent: Vec, } @@ -87,6 +90,7 @@ impl QuickOpenModal { filtered: Vec::new(), selected_index: 0, focus_handle: cx.focus_handle(), + open_files: Vec::new(), recent: Vec::new(), } } @@ -98,6 +102,22 @@ impl QuickOpenModal { 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(); @@ -150,15 +170,34 @@ impl QuickOpenModal { } } - /// 按有效查询串重算命中列表。 + /// 按有效查询串重算命中列表:已打开优先、最近其次、其余最后 + /// (对齐 Tauri `useFileSearch` 的三段分组与全局选中下标)。 fn recompute_filtered(&mut self) { let q = self.effective_query(); - self.filtered = self - .files - .iter() - .filter(|path| q.is_empty() || path.to_lowercase().contains(&q)) - .cloned() - .collect(); + 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); } @@ -326,10 +365,16 @@ impl Render for QuickOpenModal { 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. 结果列表:按 recent / other 分组,路径分段着色 + // 2. 结果列表:已打开优先、最近其次、其余最后,路径分段着色 div() .flex_1() .w_full() @@ -431,3 +476,31 @@ fn shortcut_badge(label: &str) -> AnyElement { .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/toolbar.rs b/linux/src/workbench/toolbar.rs index ef87bafd2..5a8e0d756 100644 --- a/linux/src/workbench/toolbar.rs +++ b/linux/src/workbench/toolbar.rs @@ -823,7 +823,7 @@ impl Render for ToolbarView { .small() .ghost() .icon(IconName::Search) - .tooltip(crate::i18n::menu_text(cx, "menu.quickOpen")) + .tooltip(crate::i18n::menu_text(cx, "workbench.search")) .on_click(cx.listener(|_this, _event, _window, cx| { cx.emit(ToolbarEvent::QuickOpen); })), diff --git a/linux/src/workbench/view.rs b/linux/src/workbench/view.rs index d08d6ac97..9fc7731fe 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -231,6 +231,19 @@ impl WorkbenchView { qo.set_files(file_list, cx); }); } + // 已打开标签页同步给快速打开置顶分组(对齐 Tauri `openBufferFiles`)。 + let open_files: Vec = this + .editor + .read(cx) + .tabs + .iter() + .map(|tab| tab.path.clone()) + .collect(); + if !open_files.is_empty() { + let _ = quick_open_sync.update(cx, |qo, cx| { + qo.set_open_files(open_files, cx); + }); + } // 同步 Maven 可用性:有 pom 项目时右侧插件栏才显示 Maven 入口。 let has_maven = this.maven.read(cx).has_projects(); @@ -499,6 +512,7 @@ impl WorkbenchView { // 7. 订阅快速打开事件 let status_bar_quick = status_bar.clone(); + let quick_open_recent = quick_open.clone(); let sub_quick_open = cx.subscribe( &quick_open, move |this, _qo, event: &QuickOpenEvent, cx| match event { @@ -513,6 +527,11 @@ impl WorkbenchView { cx, ); }); + // 记录最近打开(对齐 Tauri `addOrUpdateRecentFile`)。 + let recent_path = path.clone(); + let _ = quick_open_recent.update(cx, |qo, cx| { + qo.push_recent(recent_path, cx); + }); this.show_quick_open = false; cx.notify(); } @@ -1476,6 +1495,14 @@ fn this_sync_files(this: &WorkbenchView, cx: &mut Context) { if let Some(root_node) = &this.sidebar.read(cx).root_node { WorkbenchView::collect_all_file_paths(root_node, &mut file_list); } + // 已打开标签页路径同步给快速打开的置顶分组(对齐 Tauri `openBufferFiles`)。 + let open_files: Vec = this + .editor + .read(cx) + .tabs + .iter() + .map(|tab| tab.path.clone()) + .collect(); if !file_list.is_empty() { let _ = this.search_everywhere.update(cx, |search, cx| { search.set_files(file_list.clone(), cx); @@ -1484,6 +1511,11 @@ fn this_sync_files(this: &WorkbenchView, cx: &mut Context) { qo.set_files(file_list, cx); }); } + if !open_files.is_empty() { + let _ = this.quick_open.update(cx, |qo, cx| { + qo.set_open_files(open_files, cx); + }); + } } impl Render for WorkbenchView { From ff24bf3e0cca07041d09aa1b0bf140ceb450f1c9 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 23:47:57 +0800 Subject: [PATCH 27/59] =?UTF-8?q?feat(linux):=20=E5=A4=8D=E7=94=A8=20Windo?= =?UTF-8?q?ws=20lithe=20=E5=9B=BE=E6=A0=87=E9=9B=86=E7=9A=84=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=A4=B9=E4=B8=8E=E6=96=87=E4=BB=B6=E5=BD=A9=E8=89=B2?= =?UTF-8?q?=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 linux/assets/file-icons(lithe-icons SVG + map.json) - 新增 linux/build.rs:编译期生成查找表与 SVG 内嵌字节 - 新增 workbench/file_icon:对齐 Tauri 文件名/多后缀扩展名/目录名查找与深浅主题 - 侧边栏、快速打开、编辑器标签页接入,缺失回退 Lucide 单色图标 --- linux/assets/file-icons/icons/files/adobe.svg | 7 + .../assets/file-icons/icons/files/agents.svg | 7 + .../assets/file-icons/icons/files/android.svg | 7 + .../assets/file-icons/icons/files/angular.svg | 7 + .../assets/file-icons/icons/files/ansible.svg | 7 + linux/assets/file-icons/icons/files/apple.svg | 7 + .../assets/file-icons/icons/files/archive.svg | 7 + .../assets/file-icons/icons/files/arduino.svg | 7 + linux/assets/file-icons/icons/files/astro.svg | 7 + linux/assets/file-icons/icons/files/audio.svg | 7 + .../assets/file-icons/icons/files/authors.svg | 7 + linux/assets/file-icons/icons/files/babel.svg | 7 + linux/assets/file-icons/icons/files/biome.svg | 7 + .../file-icons/icons/files/bitbucket.svg | 7 + .../assets/file-icons/icons/files/blender.svg | 7 + linux/assets/file-icons/icons/files/bun.svg | 7 + linux/assets/file-icons/icons/files/c.svg | 7 + .../file-icons/icons/files/certificate.svg | 7 + .../file-icons/icons/files/changelog.svg | 7 + .../assets/file-icons/icons/files/claude.svg | 7 + linux/assets/file-icons/icons/files/cline.svg | 7 + .../assets/file-icons/icons/files/clojure.svg | 7 + .../file-icons/icons/files/cloudflare.svg | 7 + linux/assets/file-icons/icons/files/cmake.svg | 7 + linux/assets/file-icons/icons/files/codex.svg | 7 + .../file-icons/icons/files/commitlint.svg | 7 + .../assets/file-icons/icons/files/config.svg | 7 + .../assets/file-icons/icons/files/copilot.svg | 7 + linux/assets/file-icons/icons/files/cpp.svg | 7 + .../assets/file-icons/icons/files/csharp.svg | 7 + .../assets/file-icons/icons/files/cspell.svg | 7 + linux/assets/file-icons/icons/files/css.svg | 7 + linux/assets/file-icons/icons/files/csv.svg | 7 + .../assets/file-icons/icons/files/cursor.svg | 7 + .../assets/file-icons/icons/files/cypress.svg | 7 + linux/assets/file-icons/icons/files/dart.svg | 7 + .../file-icons/icons/files/database.svg | 7 + linux/assets/file-icons/icons/files/deno.svg | 7 + .../file-icons/icons/files/dependabot.svg | 7 + .../file-icons/icons/files/devcontainer.svg | 7 + linux/assets/file-icons/icons/files/diff.svg | 7 + .../assets/file-icons/icons/files/django.svg | 7 + .../file-icons/icons/files/docker-compose.svg | 7 + .../assets/file-icons/icons/files/docker.svg | 7 + .../file-icons/icons/files/document.svg | 7 + .../file-icons/icons/files/docusaurus.svg | 7 + .../assets/file-icons/icons/files/drawio.svg | 7 + .../assets/file-icons/icons/files/drizzle.svg | 7 + .../file-icons/icons/files/editorconfig.svg | 7 + .../file-icons/icons/files/electron.svg | 7 + .../assets/file-icons/icons/files/elixir.svg | 7 + linux/assets/file-icons/icons/files/env.svg | 7 + .../assets/file-icons/icons/files/erlang.svg | 7 + linux/assets/file-icons/icons/files/error.svg | 7 + .../assets/file-icons/icons/files/eslint.svg | 7 + .../file-icons/icons/files/excalidraw.svg | 7 + .../assets/file-icons/icons/files/fastapi.svg | 7 + linux/assets/file-icons/icons/files/figma.svg | 7 + linux/assets/file-icons/icons/files/file.svg | 7 + .../file-icons/icons/files/firebase.svg | 7 + linux/assets/file-icons/icons/files/flask.svg | 7 + linux/assets/file-icons/icons/files/font.svg | 7 + .../assets/file-icons/icons/files/gatsby.svg | 7 + .../assets/file-icons/icons/files/gemini.svg | 7 + linux/assets/file-icons/icons/files/git.svg | 7 + .../file-icons/icons/files/github-actions.svg | 7 + .../assets/file-icons/icons/files/github.svg | 7 + .../assets/file-icons/icons/files/gitlab.svg | 7 + linux/assets/file-icons/icons/files/go.svg | 7 + .../assets/file-icons/icons/files/gradle.svg | 7 + .../file-icons/icons/files/graphql-schema.svg | 7 + .../assets/file-icons/icons/files/graphql.svg | 7 + .../assets/file-icons/icons/files/haskell.svg | 7 + linux/assets/file-icons/icons/files/helm.svg | 7 + linux/assets/file-icons/icons/files/html.svg | 7 + linux/assets/file-icons/icons/files/http.svg | 7 + linux/assets/file-icons/icons/files/hurl.svg | 7 + linux/assets/file-icons/icons/files/image.svg | 7 + .../file-icons/icons/files/index-js.svg | 7 + .../file-icons/icons/files/index-ts.svg | 7 + linux/assets/file-icons/icons/files/java.svg | 7 + .../file-icons/icons/files/javascript.svg | 7 + .../assets/file-icons/icons/files/jenkins.svg | 7 + linux/assets/file-icons/icons/files/jest.svg | 7 + .../file-icons/icons/files/jsconfig.svg | 7 + linux/assets/file-icons/icons/files/json.svg | 7 + linux/assets/file-icons/icons/files/julia.svg | 7 + linux/assets/file-icons/icons/files/key.svg | 7 + .../assets/file-icons/icons/files/kotlin.svg | 7 + .../file-icons/icons/files/kubernetes.svg | 7 + .../assets/file-icons/icons/files/laravel.svg | 7 + .../assets/file-icons/icons/files/layout.svg | 7 + .../assets/file-icons/icons/files/license.svg | 7 + .../file-icons/icons/files/lintstaged.svg | 7 + linux/assets/file-icons/icons/files/linux.svg | 7 + linux/assets/file-icons/icons/files/lit.svg | 7 + linux/assets/file-icons/icons/files/lithe.svg | 7 + .../assets/file-icons/icons/files/loading.svg | 7 + linux/assets/file-icons/icons/files/lock.svg | 7 + linux/assets/file-icons/icons/files/log.svg | 7 + linux/assets/file-icons/icons/files/lua.svg | 7 + .../file-icons/icons/files/makefile.svg | 7 + .../file-icons/icons/files/markdown.svg | 7 + .../file-icons/icons/files/markdownlint.svg | 7 + linux/assets/file-icons/icons/files/maven.svg | 7 + linux/assets/file-icons/icons/files/mcp.svg | 7 + .../assets/file-icons/icons/files/mermaid.svg | 7 + linux/assets/file-icons/icons/files/mongo.svg | 7 + .../assets/file-icons/icons/files/netlify.svg | 7 + linux/assets/file-icons/icons/files/next.svg | 7 + linux/assets/file-icons/icons/files/nginx.svg | 7 + linux/assets/file-icons/icons/files/nim.svg | 7 + linux/assets/file-icons/icons/files/nix.svg | 7 + linux/assets/file-icons/icons/files/node.svg | 7 + .../file-icons/icons/files/not-found.svg | 7 + .../file-icons/icons/files/notebook.svg | 7 + linux/assets/file-icons/icons/files/npm.svg | 7 + linux/assets/file-icons/icons/files/nuxt.svg | 7 + linux/assets/file-icons/icons/files/nx.svg | 7 + linux/assets/file-icons/icons/files/ocaml.svg | 7 + .../assets/file-icons/icons/files/package.svg | 7 + linux/assets/file-icons/icons/files/page.svg | 7 + linux/assets/file-icons/icons/files/patch.svg | 7 + linux/assets/file-icons/icons/files/pdf.svg | 7 + linux/assets/file-icons/icons/files/perl.svg | 7 + linux/assets/file-icons/icons/files/php.svg | 7 + .../file-icons/icons/files/playwright.svg | 7 + linux/assets/file-icons/icons/files/pnpm.svg | 7 + .../file-icons/icons/files/postgres.svg | 7 + .../file-icons/icons/files/powerpoint.svg | 7 + .../file-icons/icons/files/prettier.svg | 7 + .../assets/file-icons/icons/files/prisma.svg | 7 + linux/assets/file-icons/icons/files/proto.svg | 7 + .../assets/file-icons/icons/files/python.svg | 7 + linux/assets/file-icons/icons/files/qwik.svg | 7 + linux/assets/file-icons/icons/files/r.svg | 7 + linux/assets/file-icons/icons/files/react.svg | 7 + linux/assets/file-icons/icons/files/redis.svg | 7 + linux/assets/file-icons/icons/files/remix.svg | 7 + .../file-icons/icons/files/renovate.svg | 7 + .../file-icons/icons/files/rescript.svg | 7 + .../assets/file-icons/icons/files/rollup.svg | 7 + linux/assets/file-icons/icons/files/route.svg | 7 + .../assets/file-icons/icons/files/rspack.svg | 7 + linux/assets/file-icons/icons/files/ruby.svg | 7 + linux/assets/file-icons/icons/files/rust.svg | 7 + linux/assets/file-icons/icons/files/sass.svg | 7 + linux/assets/file-icons/icons/files/scala.svg | 7 + .../file-icons/icons/files/security.svg | 7 + linux/assets/file-icons/icons/files/shell.svg | 7 + .../assets/file-icons/icons/files/sketch.svg | 7 + linux/assets/file-icons/icons/files/solid.svg | 7 + .../file-icons/icons/files/solidity.svg | 7 + .../file-icons/icons/files/spreadsheet.svg | 7 + linux/assets/file-icons/icons/files/sql.svg | 7 + .../file-icons/icons/files/storybook.svg | 7 + .../file-icons/icons/files/stylelint.svg | 7 + .../file-icons/icons/files/supabase.svg | 7 + .../assets/file-icons/icons/files/svelte.svg | 7 + linux/assets/file-icons/icons/files/svg.svg | 7 + linux/assets/file-icons/icons/files/swc.svg | 7 + linux/assets/file-icons/icons/files/swift.svg | 7 + .../file-icons/icons/files/tailwind.svg | 7 + linux/assets/file-icons/icons/files/tauri.svg | 7 + .../file-icons/icons/files/terraform.svg | 7 + linux/assets/file-icons/icons/files/test.svg | 7 + linux/assets/file-icons/icons/files/text.svg | 7 + linux/assets/file-icons/icons/files/toml.svg | 7 + .../file-icons/icons/files/turborepo.svg | 7 + .../file-icons/icons/files/typescript.svg | 7 + .../file-icons/icons/files/vercel-config.svg | 7 + .../assets/file-icons/icons/files/vercel.svg | 7 + linux/assets/file-icons/icons/files/video.svg | 7 + linux/assets/file-icons/icons/files/vite.svg | 7 + .../assets/file-icons/icons/files/vitest.svg | 7 + linux/assets/file-icons/icons/files/vue.svg | 7 + .../assets/file-icons/icons/files/warning.svg | 7 + linux/assets/file-icons/icons/files/wasm.svg | 7 + .../assets/file-icons/icons/files/webpack.svg | 7 + .../assets/file-icons/icons/files/windows.svg | 7 + linux/assets/file-icons/icons/files/word.svg | 7 + linux/assets/file-icons/icons/files/xcode.svg | 7 + linux/assets/file-icons/icons/files/xml.svg | 7 + linux/assets/file-icons/icons/files/yaml.svg | 7 + linux/assets/file-icons/icons/files/yarn.svg | 7 + linux/assets/file-icons/icons/files/zig.svg | 7 + .../icons/folders/folder-ai-open.svg | 5 + .../file-icons/icons/folders/folder-ai.svg | 5 + .../icons/folders/folder-assets-open.svg | 5 + .../icons/folders/folder-assets.svg | 5 + .../icons/folders/folder-build-open.svg | 5 + .../file-icons/icons/folders/folder-build.svg | 5 + .../icons/folders/folder-cloud-open.svg | 5 + .../file-icons/icons/folders/folder-cloud.svg | 5 + .../icons/folders/folder-components-open.svg | 5 + .../icons/folders/folder-components.svg | 5 + .../icons/folders/folder-config-open.svg | 5 + .../icons/folders/folder-config.svg | 5 + .../icons/folders/folder-database-open.svg | 5 + .../icons/folders/folder-database.svg | 5 + .../icons/folders/folder-docs-open.svg | 5 + .../file-icons/icons/folders/folder-docs.svg | 5 + .../icons/folders/folder-extensions-open.svg | 5 + .../icons/folders/folder-extensions.svg | 5 + .../icons/folders/folder-git-open.svg | 5 + .../file-icons/icons/folders/folder-git.svg | 5 + .../icons/folders/folder-locales-open.svg | 5 + .../icons/folders/folder-locales.svg | 5 + .../icons/folders/folder-mobile-open.svg | 5 + .../icons/folders/folder-mobile.svg | 5 + .../file-icons/icons/folders/folder-open.svg | 4 + .../icons/folders/folder-packages-open.svg | 5 + .../icons/folders/folder-packages.svg | 5 + .../icons/folders/folder-routes-open.svg | 5 + .../icons/folders/folder-routes.svg | 5 + .../icons/folders/folder-rust-open.svg | 5 + .../file-icons/icons/folders/folder-rust.svg | 5 + .../icons/folders/folder-scripts-open.svg | 5 + .../icons/folders/folder-scripts.svg | 5 + .../icons/folders/folder-security-open.svg | 5 + .../icons/folders/folder-security.svg | 5 + .../icons/folders/folder-source-open.svg | 5 + .../icons/folders/folder-source.svg | 5 + .../icons/folders/folder-styles-open.svg | 5 + .../icons/folders/folder-styles.svg | 5 + .../icons/folders/folder-test-open.svg | 5 + .../file-icons/icons/folders/folder-test.svg | 5 + .../file-icons/icons/folders/folder.svg | 4 + .../file-icons/icons/light/files/adobe.svg | 7 + .../file-icons/icons/light/files/agents.svg | 7 + .../file-icons/icons/light/files/android.svg | 7 + .../file-icons/icons/light/files/angular.svg | 7 + .../file-icons/icons/light/files/ansible.svg | 7 + .../file-icons/icons/light/files/apple.svg | 7 + .../file-icons/icons/light/files/archive.svg | 7 + .../file-icons/icons/light/files/arduino.svg | 7 + .../file-icons/icons/light/files/astro.svg | 7 + .../file-icons/icons/light/files/audio.svg | 7 + .../file-icons/icons/light/files/authors.svg | 7 + .../file-icons/icons/light/files/babel.svg | 7 + .../file-icons/icons/light/files/biome.svg | 7 + .../icons/light/files/bitbucket.svg | 7 + .../file-icons/icons/light/files/blender.svg | 7 + .../file-icons/icons/light/files/bun.svg | 7 + .../assets/file-icons/icons/light/files/c.svg | 7 + .../icons/light/files/certificate.svg | 7 + .../icons/light/files/changelog.svg | 7 + .../file-icons/icons/light/files/claude.svg | 7 + .../file-icons/icons/light/files/cline.svg | 7 + .../file-icons/icons/light/files/clojure.svg | 7 + .../icons/light/files/cloudflare.svg | 7 + .../file-icons/icons/light/files/cmake.svg | 7 + .../file-icons/icons/light/files/codex.svg | 7 + .../icons/light/files/commitlint.svg | 7 + .../file-icons/icons/light/files/config.svg | 7 + .../file-icons/icons/light/files/copilot.svg | 7 + .../file-icons/icons/light/files/cpp.svg | 7 + .../file-icons/icons/light/files/csharp.svg | 7 + .../file-icons/icons/light/files/cspell.svg | 7 + .../file-icons/icons/light/files/css.svg | 7 + .../file-icons/icons/light/files/csv.svg | 7 + .../file-icons/icons/light/files/cursor.svg | 7 + .../file-icons/icons/light/files/cypress.svg | 7 + .../file-icons/icons/light/files/dart.svg | 7 + .../file-icons/icons/light/files/database.svg | 7 + .../file-icons/icons/light/files/deno.svg | 7 + .../icons/light/files/dependabot.svg | 7 + .../icons/light/files/devcontainer.svg | 7 + .../file-icons/icons/light/files/diff.svg | 7 + .../file-icons/icons/light/files/django.svg | 7 + .../icons/light/files/docker-compose.svg | 7 + .../file-icons/icons/light/files/docker.svg | 7 + .../file-icons/icons/light/files/document.svg | 7 + .../icons/light/files/docusaurus.svg | 7 + .../file-icons/icons/light/files/drawio.svg | 7 + .../file-icons/icons/light/files/drizzle.svg | 7 + .../icons/light/files/editorconfig.svg | 7 + .../file-icons/icons/light/files/electron.svg | 7 + .../file-icons/icons/light/files/elixir.svg | 7 + .../file-icons/icons/light/files/env.svg | 7 + .../file-icons/icons/light/files/erlang.svg | 7 + .../file-icons/icons/light/files/error.svg | 7 + .../file-icons/icons/light/files/eslint.svg | 7 + .../icons/light/files/excalidraw.svg | 7 + .../file-icons/icons/light/files/fastapi.svg | 7 + .../file-icons/icons/light/files/figma.svg | 7 + .../file-icons/icons/light/files/file.svg | 7 + .../file-icons/icons/light/files/firebase.svg | 7 + .../file-icons/icons/light/files/flask.svg | 7 + .../file-icons/icons/light/files/font.svg | 7 + .../file-icons/icons/light/files/gatsby.svg | 7 + .../file-icons/icons/light/files/gemini.svg | 7 + .../file-icons/icons/light/files/git.svg | 7 + .../icons/light/files/github-actions.svg | 7 + .../file-icons/icons/light/files/github.svg | 7 + .../file-icons/icons/light/files/gitlab.svg | 7 + .../file-icons/icons/light/files/go.svg | 7 + .../file-icons/icons/light/files/gradle.svg | 7 + .../icons/light/files/graphql-schema.svg | 7 + .../file-icons/icons/light/files/graphql.svg | 7 + .../file-icons/icons/light/files/haskell.svg | 7 + .../file-icons/icons/light/files/helm.svg | 7 + .../file-icons/icons/light/files/html.svg | 7 + .../file-icons/icons/light/files/http.svg | 7 + .../file-icons/icons/light/files/hurl.svg | 7 + .../file-icons/icons/light/files/image.svg | 7 + .../file-icons/icons/light/files/index-js.svg | 7 + .../file-icons/icons/light/files/index-ts.svg | 7 + .../file-icons/icons/light/files/java.svg | 7 + .../icons/light/files/javascript.svg | 7 + .../file-icons/icons/light/files/jenkins.svg | 7 + .../file-icons/icons/light/files/jest.svg | 7 + .../file-icons/icons/light/files/jsconfig.svg | 7 + .../file-icons/icons/light/files/json.svg | 7 + .../file-icons/icons/light/files/julia.svg | 7 + .../file-icons/icons/light/files/key.svg | 7 + .../file-icons/icons/light/files/kotlin.svg | 7 + .../icons/light/files/kubernetes.svg | 7 + .../file-icons/icons/light/files/laravel.svg | 7 + .../file-icons/icons/light/files/layout.svg | 7 + .../file-icons/icons/light/files/license.svg | 7 + .../icons/light/files/lintstaged.svg | 7 + .../file-icons/icons/light/files/linux.svg | 7 + .../file-icons/icons/light/files/lit.svg | 7 + .../file-icons/icons/light/files/lithe.svg | 7 + .../file-icons/icons/light/files/loading.svg | 7 + .../file-icons/icons/light/files/lock.svg | 7 + .../file-icons/icons/light/files/log.svg | 7 + .../file-icons/icons/light/files/lua.svg | 7 + .../file-icons/icons/light/files/makefile.svg | 7 + .../file-icons/icons/light/files/markdown.svg | 7 + .../icons/light/files/markdownlint.svg | 7 + .../file-icons/icons/light/files/maven.svg | 7 + .../file-icons/icons/light/files/mcp.svg | 7 + .../file-icons/icons/light/files/mermaid.svg | 7 + .../file-icons/icons/light/files/mongo.svg | 7 + .../file-icons/icons/light/files/netlify.svg | 7 + .../file-icons/icons/light/files/next.svg | 7 + .../file-icons/icons/light/files/nginx.svg | 7 + .../file-icons/icons/light/files/nim.svg | 7 + .../file-icons/icons/light/files/nix.svg | 7 + .../file-icons/icons/light/files/node.svg | 7 + .../icons/light/files/not-found.svg | 7 + .../file-icons/icons/light/files/notebook.svg | 7 + .../file-icons/icons/light/files/npm.svg | 7 + .../file-icons/icons/light/files/nuxt.svg | 7 + .../file-icons/icons/light/files/nx.svg | 7 + .../file-icons/icons/light/files/ocaml.svg | 7 + .../file-icons/icons/light/files/package.svg | 7 + .../file-icons/icons/light/files/page.svg | 7 + .../file-icons/icons/light/files/patch.svg | 7 + .../file-icons/icons/light/files/pdf.svg | 7 + .../file-icons/icons/light/files/perl.svg | 7 + .../file-icons/icons/light/files/php.svg | 7 + .../icons/light/files/playwright.svg | 7 + .../file-icons/icons/light/files/pnpm.svg | 7 + .../file-icons/icons/light/files/postgres.svg | 7 + .../icons/light/files/powerpoint.svg | 7 + .../file-icons/icons/light/files/prettier.svg | 7 + .../file-icons/icons/light/files/prisma.svg | 7 + .../file-icons/icons/light/files/proto.svg | 7 + .../file-icons/icons/light/files/python.svg | 7 + .../file-icons/icons/light/files/qwik.svg | 7 + .../assets/file-icons/icons/light/files/r.svg | 7 + .../file-icons/icons/light/files/react.svg | 7 + .../file-icons/icons/light/files/redis.svg | 7 + .../file-icons/icons/light/files/remix.svg | 7 + .../file-icons/icons/light/files/renovate.svg | 7 + .../file-icons/icons/light/files/rescript.svg | 7 + .../file-icons/icons/light/files/rollup.svg | 7 + .../file-icons/icons/light/files/route.svg | 7 + .../file-icons/icons/light/files/rspack.svg | 7 + .../file-icons/icons/light/files/ruby.svg | 7 + .../file-icons/icons/light/files/rust.svg | 7 + .../file-icons/icons/light/files/sass.svg | 7 + .../file-icons/icons/light/files/scala.svg | 7 + .../file-icons/icons/light/files/security.svg | 7 + .../file-icons/icons/light/files/shell.svg | 7 + .../file-icons/icons/light/files/sketch.svg | 7 + .../file-icons/icons/light/files/solid.svg | 7 + .../file-icons/icons/light/files/solidity.svg | 7 + .../icons/light/files/spreadsheet.svg | 7 + .../file-icons/icons/light/files/sql.svg | 7 + .../icons/light/files/storybook.svg | 7 + .../icons/light/files/stylelint.svg | 7 + .../file-icons/icons/light/files/supabase.svg | 7 + .../file-icons/icons/light/files/svelte.svg | 7 + .../file-icons/icons/light/files/svg.svg | 7 + .../file-icons/icons/light/files/swc.svg | 7 + .../file-icons/icons/light/files/swift.svg | 7 + .../file-icons/icons/light/files/tailwind.svg | 7 + .../file-icons/icons/light/files/tauri.svg | 7 + .../icons/light/files/terraform.svg | 7 + .../file-icons/icons/light/files/test.svg | 7 + .../file-icons/icons/light/files/text.svg | 7 + .../file-icons/icons/light/files/toml.svg | 7 + .../icons/light/files/turborepo.svg | 7 + .../icons/light/files/typescript.svg | 7 + .../icons/light/files/vercel-config.svg | 7 + .../file-icons/icons/light/files/vercel.svg | 7 + .../file-icons/icons/light/files/video.svg | 7 + .../file-icons/icons/light/files/vite.svg | 7 + .../file-icons/icons/light/files/vitest.svg | 7 + .../file-icons/icons/light/files/vue.svg | 7 + .../file-icons/icons/light/files/warning.svg | 7 + .../file-icons/icons/light/files/wasm.svg | 7 + .../file-icons/icons/light/files/webpack.svg | 7 + .../file-icons/icons/light/files/windows.svg | 7 + .../file-icons/icons/light/files/word.svg | 7 + .../file-icons/icons/light/files/xcode.svg | 7 + .../file-icons/icons/light/files/xml.svg | 7 + .../file-icons/icons/light/files/yaml.svg | 7 + .../file-icons/icons/light/files/yarn.svg | 7 + .../file-icons/icons/light/files/zig.svg | 7 + .../icons/light/folders/folder-ai-open.svg | 5 + .../icons/light/folders/folder-ai.svg | 5 + .../light/folders/folder-assets-open.svg | 5 + .../icons/light/folders/folder-assets.svg | 5 + .../icons/light/folders/folder-build-open.svg | 5 + .../icons/light/folders/folder-build.svg | 5 + .../icons/light/folders/folder-cloud-open.svg | 5 + .../icons/light/folders/folder-cloud.svg | 5 + .../light/folders/folder-components-open.svg | 5 + .../icons/light/folders/folder-components.svg | 5 + .../light/folders/folder-config-open.svg | 5 + .../icons/light/folders/folder-config.svg | 5 + .../light/folders/folder-database-open.svg | 5 + .../icons/light/folders/folder-database.svg | 5 + .../icons/light/folders/folder-docs-open.svg | 5 + .../icons/light/folders/folder-docs.svg | 5 + .../light/folders/folder-extensions-open.svg | 5 + .../icons/light/folders/folder-extensions.svg | 5 + .../icons/light/folders/folder-git-open.svg | 5 + .../icons/light/folders/folder-git.svg | 5 + .../light/folders/folder-locales-open.svg | 5 + .../icons/light/folders/folder-locales.svg | 5 + .../light/folders/folder-mobile-open.svg | 5 + .../icons/light/folders/folder-mobile.svg | 5 + .../icons/light/folders/folder-open.svg | 4 + .../light/folders/folder-packages-open.svg | 5 + .../icons/light/folders/folder-packages.svg | 5 + .../light/folders/folder-routes-open.svg | 5 + .../icons/light/folders/folder-routes.svg | 5 + .../icons/light/folders/folder-rust-open.svg | 5 + .../icons/light/folders/folder-rust.svg | 5 + .../light/folders/folder-scripts-open.svg | 5 + .../icons/light/folders/folder-scripts.svg | 5 + .../light/folders/folder-security-open.svg | 5 + .../icons/light/folders/folder-security.svg | 5 + .../light/folders/folder-source-open.svg | 5 + .../icons/light/folders/folder-source.svg | 5 + .../light/folders/folder-styles-open.svg | 5 + .../icons/light/folders/folder-styles.svg | 5 + .../icons/light/folders/folder-test-open.svg | 5 + .../icons/light/folders/folder-test.svg | 5 + .../file-icons/icons/light/folders/folder.svg | 4 + linux/assets/file-icons/map.json | 1 + linux/build.rs | 325 ++++++++++++++++++ linux/src/workbench/editor.rs | 39 ++- linux/src/workbench/file_icon.rs | 107 ++++++ linux/src/workbench/mod.rs | 3 + linux/src/workbench/quick_open.rs | 35 +- linux/src/workbench/sidebar.rs | 72 +++- 463 files changed, 3570 insertions(+), 32 deletions(-) create mode 100644 linux/assets/file-icons/icons/files/adobe.svg create mode 100644 linux/assets/file-icons/icons/files/agents.svg create mode 100644 linux/assets/file-icons/icons/files/android.svg create mode 100644 linux/assets/file-icons/icons/files/angular.svg create mode 100644 linux/assets/file-icons/icons/files/ansible.svg create mode 100644 linux/assets/file-icons/icons/files/apple.svg create mode 100644 linux/assets/file-icons/icons/files/archive.svg create mode 100644 linux/assets/file-icons/icons/files/arduino.svg create mode 100644 linux/assets/file-icons/icons/files/astro.svg create mode 100644 linux/assets/file-icons/icons/files/audio.svg create mode 100644 linux/assets/file-icons/icons/files/authors.svg create mode 100644 linux/assets/file-icons/icons/files/babel.svg create mode 100644 linux/assets/file-icons/icons/files/biome.svg create mode 100644 linux/assets/file-icons/icons/files/bitbucket.svg create mode 100644 linux/assets/file-icons/icons/files/blender.svg create mode 100644 linux/assets/file-icons/icons/files/bun.svg create mode 100644 linux/assets/file-icons/icons/files/c.svg create mode 100644 linux/assets/file-icons/icons/files/certificate.svg create mode 100644 linux/assets/file-icons/icons/files/changelog.svg create mode 100644 linux/assets/file-icons/icons/files/claude.svg create mode 100644 linux/assets/file-icons/icons/files/cline.svg create mode 100644 linux/assets/file-icons/icons/files/clojure.svg create mode 100644 linux/assets/file-icons/icons/files/cloudflare.svg create mode 100644 linux/assets/file-icons/icons/files/cmake.svg create mode 100644 linux/assets/file-icons/icons/files/codex.svg create mode 100644 linux/assets/file-icons/icons/files/commitlint.svg create mode 100644 linux/assets/file-icons/icons/files/config.svg create mode 100644 linux/assets/file-icons/icons/files/copilot.svg create mode 100644 linux/assets/file-icons/icons/files/cpp.svg create mode 100644 linux/assets/file-icons/icons/files/csharp.svg create mode 100644 linux/assets/file-icons/icons/files/cspell.svg create mode 100644 linux/assets/file-icons/icons/files/css.svg create mode 100644 linux/assets/file-icons/icons/files/csv.svg create mode 100644 linux/assets/file-icons/icons/files/cursor.svg create mode 100644 linux/assets/file-icons/icons/files/cypress.svg create mode 100644 linux/assets/file-icons/icons/files/dart.svg create mode 100644 linux/assets/file-icons/icons/files/database.svg create mode 100644 linux/assets/file-icons/icons/files/deno.svg create mode 100644 linux/assets/file-icons/icons/files/dependabot.svg create mode 100644 linux/assets/file-icons/icons/files/devcontainer.svg create mode 100644 linux/assets/file-icons/icons/files/diff.svg create mode 100644 linux/assets/file-icons/icons/files/django.svg create mode 100644 linux/assets/file-icons/icons/files/docker-compose.svg create mode 100644 linux/assets/file-icons/icons/files/docker.svg create mode 100644 linux/assets/file-icons/icons/files/document.svg create mode 100644 linux/assets/file-icons/icons/files/docusaurus.svg create mode 100644 linux/assets/file-icons/icons/files/drawio.svg create mode 100644 linux/assets/file-icons/icons/files/drizzle.svg create mode 100644 linux/assets/file-icons/icons/files/editorconfig.svg create mode 100644 linux/assets/file-icons/icons/files/electron.svg create mode 100644 linux/assets/file-icons/icons/files/elixir.svg create mode 100644 linux/assets/file-icons/icons/files/env.svg create mode 100644 linux/assets/file-icons/icons/files/erlang.svg create mode 100644 linux/assets/file-icons/icons/files/error.svg create mode 100644 linux/assets/file-icons/icons/files/eslint.svg create mode 100644 linux/assets/file-icons/icons/files/excalidraw.svg create mode 100644 linux/assets/file-icons/icons/files/fastapi.svg create mode 100644 linux/assets/file-icons/icons/files/figma.svg create mode 100644 linux/assets/file-icons/icons/files/file.svg create mode 100644 linux/assets/file-icons/icons/files/firebase.svg create mode 100644 linux/assets/file-icons/icons/files/flask.svg create mode 100644 linux/assets/file-icons/icons/files/font.svg create mode 100644 linux/assets/file-icons/icons/files/gatsby.svg create mode 100644 linux/assets/file-icons/icons/files/gemini.svg create mode 100644 linux/assets/file-icons/icons/files/git.svg create mode 100644 linux/assets/file-icons/icons/files/github-actions.svg create mode 100644 linux/assets/file-icons/icons/files/github.svg create mode 100644 linux/assets/file-icons/icons/files/gitlab.svg create mode 100644 linux/assets/file-icons/icons/files/go.svg create mode 100644 linux/assets/file-icons/icons/files/gradle.svg create mode 100644 linux/assets/file-icons/icons/files/graphql-schema.svg create mode 100644 linux/assets/file-icons/icons/files/graphql.svg create mode 100644 linux/assets/file-icons/icons/files/haskell.svg create mode 100644 linux/assets/file-icons/icons/files/helm.svg create mode 100644 linux/assets/file-icons/icons/files/html.svg create mode 100644 linux/assets/file-icons/icons/files/http.svg create mode 100644 linux/assets/file-icons/icons/files/hurl.svg create mode 100644 linux/assets/file-icons/icons/files/image.svg create mode 100644 linux/assets/file-icons/icons/files/index-js.svg create mode 100644 linux/assets/file-icons/icons/files/index-ts.svg create mode 100644 linux/assets/file-icons/icons/files/java.svg create mode 100644 linux/assets/file-icons/icons/files/javascript.svg create mode 100644 linux/assets/file-icons/icons/files/jenkins.svg create mode 100644 linux/assets/file-icons/icons/files/jest.svg create mode 100644 linux/assets/file-icons/icons/files/jsconfig.svg create mode 100644 linux/assets/file-icons/icons/files/json.svg create mode 100644 linux/assets/file-icons/icons/files/julia.svg create mode 100644 linux/assets/file-icons/icons/files/key.svg create mode 100644 linux/assets/file-icons/icons/files/kotlin.svg create mode 100644 linux/assets/file-icons/icons/files/kubernetes.svg create mode 100644 linux/assets/file-icons/icons/files/laravel.svg create mode 100644 linux/assets/file-icons/icons/files/layout.svg create mode 100644 linux/assets/file-icons/icons/files/license.svg create mode 100644 linux/assets/file-icons/icons/files/lintstaged.svg create mode 100644 linux/assets/file-icons/icons/files/linux.svg create mode 100644 linux/assets/file-icons/icons/files/lit.svg create mode 100644 linux/assets/file-icons/icons/files/lithe.svg create mode 100644 linux/assets/file-icons/icons/files/loading.svg create mode 100644 linux/assets/file-icons/icons/files/lock.svg create mode 100644 linux/assets/file-icons/icons/files/log.svg create mode 100644 linux/assets/file-icons/icons/files/lua.svg create mode 100644 linux/assets/file-icons/icons/files/makefile.svg create mode 100644 linux/assets/file-icons/icons/files/markdown.svg create mode 100644 linux/assets/file-icons/icons/files/markdownlint.svg create mode 100644 linux/assets/file-icons/icons/files/maven.svg create mode 100644 linux/assets/file-icons/icons/files/mcp.svg create mode 100644 linux/assets/file-icons/icons/files/mermaid.svg create mode 100644 linux/assets/file-icons/icons/files/mongo.svg create mode 100644 linux/assets/file-icons/icons/files/netlify.svg create mode 100644 linux/assets/file-icons/icons/files/next.svg create mode 100644 linux/assets/file-icons/icons/files/nginx.svg create mode 100644 linux/assets/file-icons/icons/files/nim.svg create mode 100644 linux/assets/file-icons/icons/files/nix.svg create mode 100644 linux/assets/file-icons/icons/files/node.svg create mode 100644 linux/assets/file-icons/icons/files/not-found.svg create mode 100644 linux/assets/file-icons/icons/files/notebook.svg create mode 100644 linux/assets/file-icons/icons/files/npm.svg create mode 100644 linux/assets/file-icons/icons/files/nuxt.svg create mode 100644 linux/assets/file-icons/icons/files/nx.svg create mode 100644 linux/assets/file-icons/icons/files/ocaml.svg create mode 100644 linux/assets/file-icons/icons/files/package.svg create mode 100644 linux/assets/file-icons/icons/files/page.svg create mode 100644 linux/assets/file-icons/icons/files/patch.svg create mode 100644 linux/assets/file-icons/icons/files/pdf.svg create mode 100644 linux/assets/file-icons/icons/files/perl.svg create mode 100644 linux/assets/file-icons/icons/files/php.svg create mode 100644 linux/assets/file-icons/icons/files/playwright.svg create mode 100644 linux/assets/file-icons/icons/files/pnpm.svg create mode 100644 linux/assets/file-icons/icons/files/postgres.svg create mode 100644 linux/assets/file-icons/icons/files/powerpoint.svg create mode 100644 linux/assets/file-icons/icons/files/prettier.svg create mode 100644 linux/assets/file-icons/icons/files/prisma.svg create mode 100644 linux/assets/file-icons/icons/files/proto.svg create mode 100644 linux/assets/file-icons/icons/files/python.svg create mode 100644 linux/assets/file-icons/icons/files/qwik.svg create mode 100644 linux/assets/file-icons/icons/files/r.svg create mode 100644 linux/assets/file-icons/icons/files/react.svg create mode 100644 linux/assets/file-icons/icons/files/redis.svg create mode 100644 linux/assets/file-icons/icons/files/remix.svg create mode 100644 linux/assets/file-icons/icons/files/renovate.svg create mode 100644 linux/assets/file-icons/icons/files/rescript.svg create mode 100644 linux/assets/file-icons/icons/files/rollup.svg create mode 100644 linux/assets/file-icons/icons/files/route.svg create mode 100644 linux/assets/file-icons/icons/files/rspack.svg create mode 100644 linux/assets/file-icons/icons/files/ruby.svg create mode 100644 linux/assets/file-icons/icons/files/rust.svg create mode 100644 linux/assets/file-icons/icons/files/sass.svg create mode 100644 linux/assets/file-icons/icons/files/scala.svg create mode 100644 linux/assets/file-icons/icons/files/security.svg create mode 100644 linux/assets/file-icons/icons/files/shell.svg create mode 100644 linux/assets/file-icons/icons/files/sketch.svg create mode 100644 linux/assets/file-icons/icons/files/solid.svg create mode 100644 linux/assets/file-icons/icons/files/solidity.svg create mode 100644 linux/assets/file-icons/icons/files/spreadsheet.svg create mode 100644 linux/assets/file-icons/icons/files/sql.svg create mode 100644 linux/assets/file-icons/icons/files/storybook.svg create mode 100644 linux/assets/file-icons/icons/files/stylelint.svg create mode 100644 linux/assets/file-icons/icons/files/supabase.svg create mode 100644 linux/assets/file-icons/icons/files/svelte.svg create mode 100644 linux/assets/file-icons/icons/files/svg.svg create mode 100644 linux/assets/file-icons/icons/files/swc.svg create mode 100644 linux/assets/file-icons/icons/files/swift.svg create mode 100644 linux/assets/file-icons/icons/files/tailwind.svg create mode 100644 linux/assets/file-icons/icons/files/tauri.svg create mode 100644 linux/assets/file-icons/icons/files/terraform.svg create mode 100644 linux/assets/file-icons/icons/files/test.svg create mode 100644 linux/assets/file-icons/icons/files/text.svg create mode 100644 linux/assets/file-icons/icons/files/toml.svg create mode 100644 linux/assets/file-icons/icons/files/turborepo.svg create mode 100644 linux/assets/file-icons/icons/files/typescript.svg create mode 100644 linux/assets/file-icons/icons/files/vercel-config.svg create mode 100644 linux/assets/file-icons/icons/files/vercel.svg create mode 100644 linux/assets/file-icons/icons/files/video.svg create mode 100644 linux/assets/file-icons/icons/files/vite.svg create mode 100644 linux/assets/file-icons/icons/files/vitest.svg create mode 100644 linux/assets/file-icons/icons/files/vue.svg create mode 100644 linux/assets/file-icons/icons/files/warning.svg create mode 100644 linux/assets/file-icons/icons/files/wasm.svg create mode 100644 linux/assets/file-icons/icons/files/webpack.svg create mode 100644 linux/assets/file-icons/icons/files/windows.svg create mode 100644 linux/assets/file-icons/icons/files/word.svg create mode 100644 linux/assets/file-icons/icons/files/xcode.svg create mode 100644 linux/assets/file-icons/icons/files/xml.svg create mode 100644 linux/assets/file-icons/icons/files/yaml.svg create mode 100644 linux/assets/file-icons/icons/files/yarn.svg create mode 100644 linux/assets/file-icons/icons/files/zig.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-ai-open.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-ai.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-assets-open.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-assets.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-build-open.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-build.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-cloud-open.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-cloud.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-components-open.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-components.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-config-open.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-config.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-database-open.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-database.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-docs-open.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-docs.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-extensions-open.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-extensions.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-git-open.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-git.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-locales-open.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-locales.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-mobile-open.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-mobile.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-open.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-packages-open.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-packages.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-routes-open.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-routes.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-rust-open.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-rust.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-scripts-open.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-scripts.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-security-open.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-security.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-source-open.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-source.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-styles-open.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-styles.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-test-open.svg create mode 100644 linux/assets/file-icons/icons/folders/folder-test.svg create mode 100644 linux/assets/file-icons/icons/folders/folder.svg create mode 100644 linux/assets/file-icons/icons/light/files/adobe.svg create mode 100644 linux/assets/file-icons/icons/light/files/agents.svg create mode 100644 linux/assets/file-icons/icons/light/files/android.svg create mode 100644 linux/assets/file-icons/icons/light/files/angular.svg create mode 100644 linux/assets/file-icons/icons/light/files/ansible.svg create mode 100644 linux/assets/file-icons/icons/light/files/apple.svg create mode 100644 linux/assets/file-icons/icons/light/files/archive.svg create mode 100644 linux/assets/file-icons/icons/light/files/arduino.svg create mode 100644 linux/assets/file-icons/icons/light/files/astro.svg create mode 100644 linux/assets/file-icons/icons/light/files/audio.svg create mode 100644 linux/assets/file-icons/icons/light/files/authors.svg create mode 100644 linux/assets/file-icons/icons/light/files/babel.svg create mode 100644 linux/assets/file-icons/icons/light/files/biome.svg create mode 100644 linux/assets/file-icons/icons/light/files/bitbucket.svg create mode 100644 linux/assets/file-icons/icons/light/files/blender.svg create mode 100644 linux/assets/file-icons/icons/light/files/bun.svg create mode 100644 linux/assets/file-icons/icons/light/files/c.svg create mode 100644 linux/assets/file-icons/icons/light/files/certificate.svg create mode 100644 linux/assets/file-icons/icons/light/files/changelog.svg create mode 100644 linux/assets/file-icons/icons/light/files/claude.svg create mode 100644 linux/assets/file-icons/icons/light/files/cline.svg create mode 100644 linux/assets/file-icons/icons/light/files/clojure.svg create mode 100644 linux/assets/file-icons/icons/light/files/cloudflare.svg create mode 100644 linux/assets/file-icons/icons/light/files/cmake.svg create mode 100644 linux/assets/file-icons/icons/light/files/codex.svg create mode 100644 linux/assets/file-icons/icons/light/files/commitlint.svg create mode 100644 linux/assets/file-icons/icons/light/files/config.svg create mode 100644 linux/assets/file-icons/icons/light/files/copilot.svg create mode 100644 linux/assets/file-icons/icons/light/files/cpp.svg create mode 100644 linux/assets/file-icons/icons/light/files/csharp.svg create mode 100644 linux/assets/file-icons/icons/light/files/cspell.svg create mode 100644 linux/assets/file-icons/icons/light/files/css.svg create mode 100644 linux/assets/file-icons/icons/light/files/csv.svg create mode 100644 linux/assets/file-icons/icons/light/files/cursor.svg create mode 100644 linux/assets/file-icons/icons/light/files/cypress.svg create mode 100644 linux/assets/file-icons/icons/light/files/dart.svg create mode 100644 linux/assets/file-icons/icons/light/files/database.svg create mode 100644 linux/assets/file-icons/icons/light/files/deno.svg create mode 100644 linux/assets/file-icons/icons/light/files/dependabot.svg create mode 100644 linux/assets/file-icons/icons/light/files/devcontainer.svg create mode 100644 linux/assets/file-icons/icons/light/files/diff.svg create mode 100644 linux/assets/file-icons/icons/light/files/django.svg create mode 100644 linux/assets/file-icons/icons/light/files/docker-compose.svg create mode 100644 linux/assets/file-icons/icons/light/files/docker.svg create mode 100644 linux/assets/file-icons/icons/light/files/document.svg create mode 100644 linux/assets/file-icons/icons/light/files/docusaurus.svg create mode 100644 linux/assets/file-icons/icons/light/files/drawio.svg create mode 100644 linux/assets/file-icons/icons/light/files/drizzle.svg create mode 100644 linux/assets/file-icons/icons/light/files/editorconfig.svg create mode 100644 linux/assets/file-icons/icons/light/files/electron.svg create mode 100644 linux/assets/file-icons/icons/light/files/elixir.svg create mode 100644 linux/assets/file-icons/icons/light/files/env.svg create mode 100644 linux/assets/file-icons/icons/light/files/erlang.svg create mode 100644 linux/assets/file-icons/icons/light/files/error.svg create mode 100644 linux/assets/file-icons/icons/light/files/eslint.svg create mode 100644 linux/assets/file-icons/icons/light/files/excalidraw.svg create mode 100644 linux/assets/file-icons/icons/light/files/fastapi.svg create mode 100644 linux/assets/file-icons/icons/light/files/figma.svg create mode 100644 linux/assets/file-icons/icons/light/files/file.svg create mode 100644 linux/assets/file-icons/icons/light/files/firebase.svg create mode 100644 linux/assets/file-icons/icons/light/files/flask.svg create mode 100644 linux/assets/file-icons/icons/light/files/font.svg create mode 100644 linux/assets/file-icons/icons/light/files/gatsby.svg create mode 100644 linux/assets/file-icons/icons/light/files/gemini.svg create mode 100644 linux/assets/file-icons/icons/light/files/git.svg create mode 100644 linux/assets/file-icons/icons/light/files/github-actions.svg create mode 100644 linux/assets/file-icons/icons/light/files/github.svg create mode 100644 linux/assets/file-icons/icons/light/files/gitlab.svg create mode 100644 linux/assets/file-icons/icons/light/files/go.svg create mode 100644 linux/assets/file-icons/icons/light/files/gradle.svg create mode 100644 linux/assets/file-icons/icons/light/files/graphql-schema.svg create mode 100644 linux/assets/file-icons/icons/light/files/graphql.svg create mode 100644 linux/assets/file-icons/icons/light/files/haskell.svg create mode 100644 linux/assets/file-icons/icons/light/files/helm.svg create mode 100644 linux/assets/file-icons/icons/light/files/html.svg create mode 100644 linux/assets/file-icons/icons/light/files/http.svg create mode 100644 linux/assets/file-icons/icons/light/files/hurl.svg create mode 100644 linux/assets/file-icons/icons/light/files/image.svg create mode 100644 linux/assets/file-icons/icons/light/files/index-js.svg create mode 100644 linux/assets/file-icons/icons/light/files/index-ts.svg create mode 100644 linux/assets/file-icons/icons/light/files/java.svg create mode 100644 linux/assets/file-icons/icons/light/files/javascript.svg create mode 100644 linux/assets/file-icons/icons/light/files/jenkins.svg create mode 100644 linux/assets/file-icons/icons/light/files/jest.svg create mode 100644 linux/assets/file-icons/icons/light/files/jsconfig.svg create mode 100644 linux/assets/file-icons/icons/light/files/json.svg create mode 100644 linux/assets/file-icons/icons/light/files/julia.svg create mode 100644 linux/assets/file-icons/icons/light/files/key.svg create mode 100644 linux/assets/file-icons/icons/light/files/kotlin.svg create mode 100644 linux/assets/file-icons/icons/light/files/kubernetes.svg create mode 100644 linux/assets/file-icons/icons/light/files/laravel.svg create mode 100644 linux/assets/file-icons/icons/light/files/layout.svg create mode 100644 linux/assets/file-icons/icons/light/files/license.svg create mode 100644 linux/assets/file-icons/icons/light/files/lintstaged.svg create mode 100644 linux/assets/file-icons/icons/light/files/linux.svg create mode 100644 linux/assets/file-icons/icons/light/files/lit.svg create mode 100644 linux/assets/file-icons/icons/light/files/lithe.svg create mode 100644 linux/assets/file-icons/icons/light/files/loading.svg create mode 100644 linux/assets/file-icons/icons/light/files/lock.svg create mode 100644 linux/assets/file-icons/icons/light/files/log.svg create mode 100644 linux/assets/file-icons/icons/light/files/lua.svg create mode 100644 linux/assets/file-icons/icons/light/files/makefile.svg create mode 100644 linux/assets/file-icons/icons/light/files/markdown.svg create mode 100644 linux/assets/file-icons/icons/light/files/markdownlint.svg create mode 100644 linux/assets/file-icons/icons/light/files/maven.svg create mode 100644 linux/assets/file-icons/icons/light/files/mcp.svg create mode 100644 linux/assets/file-icons/icons/light/files/mermaid.svg create mode 100644 linux/assets/file-icons/icons/light/files/mongo.svg create mode 100644 linux/assets/file-icons/icons/light/files/netlify.svg create mode 100644 linux/assets/file-icons/icons/light/files/next.svg create mode 100644 linux/assets/file-icons/icons/light/files/nginx.svg create mode 100644 linux/assets/file-icons/icons/light/files/nim.svg create mode 100644 linux/assets/file-icons/icons/light/files/nix.svg create mode 100644 linux/assets/file-icons/icons/light/files/node.svg create mode 100644 linux/assets/file-icons/icons/light/files/not-found.svg create mode 100644 linux/assets/file-icons/icons/light/files/notebook.svg create mode 100644 linux/assets/file-icons/icons/light/files/npm.svg create mode 100644 linux/assets/file-icons/icons/light/files/nuxt.svg create mode 100644 linux/assets/file-icons/icons/light/files/nx.svg create mode 100644 linux/assets/file-icons/icons/light/files/ocaml.svg create mode 100644 linux/assets/file-icons/icons/light/files/package.svg create mode 100644 linux/assets/file-icons/icons/light/files/page.svg create mode 100644 linux/assets/file-icons/icons/light/files/patch.svg create mode 100644 linux/assets/file-icons/icons/light/files/pdf.svg create mode 100644 linux/assets/file-icons/icons/light/files/perl.svg create mode 100644 linux/assets/file-icons/icons/light/files/php.svg create mode 100644 linux/assets/file-icons/icons/light/files/playwright.svg create mode 100644 linux/assets/file-icons/icons/light/files/pnpm.svg create mode 100644 linux/assets/file-icons/icons/light/files/postgres.svg create mode 100644 linux/assets/file-icons/icons/light/files/powerpoint.svg create mode 100644 linux/assets/file-icons/icons/light/files/prettier.svg create mode 100644 linux/assets/file-icons/icons/light/files/prisma.svg create mode 100644 linux/assets/file-icons/icons/light/files/proto.svg create mode 100644 linux/assets/file-icons/icons/light/files/python.svg create mode 100644 linux/assets/file-icons/icons/light/files/qwik.svg create mode 100644 linux/assets/file-icons/icons/light/files/r.svg create mode 100644 linux/assets/file-icons/icons/light/files/react.svg create mode 100644 linux/assets/file-icons/icons/light/files/redis.svg create mode 100644 linux/assets/file-icons/icons/light/files/remix.svg create mode 100644 linux/assets/file-icons/icons/light/files/renovate.svg create mode 100644 linux/assets/file-icons/icons/light/files/rescript.svg create mode 100644 linux/assets/file-icons/icons/light/files/rollup.svg create mode 100644 linux/assets/file-icons/icons/light/files/route.svg create mode 100644 linux/assets/file-icons/icons/light/files/rspack.svg create mode 100644 linux/assets/file-icons/icons/light/files/ruby.svg create mode 100644 linux/assets/file-icons/icons/light/files/rust.svg create mode 100644 linux/assets/file-icons/icons/light/files/sass.svg create mode 100644 linux/assets/file-icons/icons/light/files/scala.svg create mode 100644 linux/assets/file-icons/icons/light/files/security.svg create mode 100644 linux/assets/file-icons/icons/light/files/shell.svg create mode 100644 linux/assets/file-icons/icons/light/files/sketch.svg create mode 100644 linux/assets/file-icons/icons/light/files/solid.svg create mode 100644 linux/assets/file-icons/icons/light/files/solidity.svg create mode 100644 linux/assets/file-icons/icons/light/files/spreadsheet.svg create mode 100644 linux/assets/file-icons/icons/light/files/sql.svg create mode 100644 linux/assets/file-icons/icons/light/files/storybook.svg create mode 100644 linux/assets/file-icons/icons/light/files/stylelint.svg create mode 100644 linux/assets/file-icons/icons/light/files/supabase.svg create mode 100644 linux/assets/file-icons/icons/light/files/svelte.svg create mode 100644 linux/assets/file-icons/icons/light/files/svg.svg create mode 100644 linux/assets/file-icons/icons/light/files/swc.svg create mode 100644 linux/assets/file-icons/icons/light/files/swift.svg create mode 100644 linux/assets/file-icons/icons/light/files/tailwind.svg create mode 100644 linux/assets/file-icons/icons/light/files/tauri.svg create mode 100644 linux/assets/file-icons/icons/light/files/terraform.svg create mode 100644 linux/assets/file-icons/icons/light/files/test.svg create mode 100644 linux/assets/file-icons/icons/light/files/text.svg create mode 100644 linux/assets/file-icons/icons/light/files/toml.svg create mode 100644 linux/assets/file-icons/icons/light/files/turborepo.svg create mode 100644 linux/assets/file-icons/icons/light/files/typescript.svg create mode 100644 linux/assets/file-icons/icons/light/files/vercel-config.svg create mode 100644 linux/assets/file-icons/icons/light/files/vercel.svg create mode 100644 linux/assets/file-icons/icons/light/files/video.svg create mode 100644 linux/assets/file-icons/icons/light/files/vite.svg create mode 100644 linux/assets/file-icons/icons/light/files/vitest.svg create mode 100644 linux/assets/file-icons/icons/light/files/vue.svg create mode 100644 linux/assets/file-icons/icons/light/files/warning.svg create mode 100644 linux/assets/file-icons/icons/light/files/wasm.svg create mode 100644 linux/assets/file-icons/icons/light/files/webpack.svg create mode 100644 linux/assets/file-icons/icons/light/files/windows.svg create mode 100644 linux/assets/file-icons/icons/light/files/word.svg create mode 100644 linux/assets/file-icons/icons/light/files/xcode.svg create mode 100644 linux/assets/file-icons/icons/light/files/xml.svg create mode 100644 linux/assets/file-icons/icons/light/files/yaml.svg create mode 100644 linux/assets/file-icons/icons/light/files/yarn.svg create mode 100644 linux/assets/file-icons/icons/light/files/zig.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-ai-open.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-ai.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-assets-open.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-assets.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-build-open.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-build.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-cloud-open.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-cloud.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-components-open.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-components.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-config-open.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-config.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-database-open.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-database.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-docs-open.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-docs.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-extensions-open.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-extensions.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-git-open.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-git.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-locales-open.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-locales.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-mobile-open.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-mobile.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-open.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-packages-open.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-packages.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-routes-open.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-routes.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-rust-open.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-rust.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-scripts-open.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-scripts.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-security-open.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-security.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-source-open.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-source.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-styles-open.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-styles.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-test-open.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder-test.svg create mode 100644 linux/assets/file-icons/icons/light/folders/folder.svg create mode 100644 linux/assets/file-icons/map.json create mode 100644 linux/build.rs create mode 100644 linux/src/workbench/file_icon.rs diff --git a/linux/assets/file-icons/icons/files/adobe.svg b/linux/assets/file-icons/icons/files/adobe.svg new file mode 100644 index 000000000..b201a3451 --- /dev/null +++ b/linux/assets/file-icons/icons/files/adobe.svg @@ -0,0 +1,7 @@ + + + + + + A + diff --git a/linux/assets/file-icons/icons/files/agents.svg b/linux/assets/file-icons/icons/files/agents.svg new file mode 100644 index 000000000..6d6d2df2f --- /dev/null +++ b/linux/assets/file-icons/icons/files/agents.svg @@ -0,0 +1,7 @@ + + + + + + AI + diff --git a/linux/assets/file-icons/icons/files/android.svg b/linux/assets/file-icons/icons/files/android.svg new file mode 100644 index 000000000..231f6d43d --- /dev/null +++ b/linux/assets/file-icons/icons/files/android.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/angular.svg b/linux/assets/file-icons/icons/files/angular.svg new file mode 100644 index 000000000..30fec93cc --- /dev/null +++ b/linux/assets/file-icons/icons/files/angular.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/ansible.svg b/linux/assets/file-icons/icons/files/ansible.svg new file mode 100644 index 000000000..fff9e0b79 --- /dev/null +++ b/linux/assets/file-icons/icons/files/ansible.svg @@ -0,0 +1,7 @@ + + + + + + A + diff --git a/linux/assets/file-icons/icons/files/apple.svg b/linux/assets/file-icons/icons/files/apple.svg new file mode 100644 index 000000000..a34cd9b5c --- /dev/null +++ b/linux/assets/file-icons/icons/files/apple.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/archive.svg b/linux/assets/file-icons/icons/files/archive.svg new file mode 100644 index 000000000..d3822e3c8 --- /dev/null +++ b/linux/assets/file-icons/icons/files/archive.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/arduino.svg b/linux/assets/file-icons/icons/files/arduino.svg new file mode 100644 index 000000000..63b1d77eb --- /dev/null +++ b/linux/assets/file-icons/icons/files/arduino.svg @@ -0,0 +1,7 @@ + + + + + + A + diff --git a/linux/assets/file-icons/icons/files/astro.svg b/linux/assets/file-icons/icons/files/astro.svg new file mode 100644 index 000000000..ecb1c91ec --- /dev/null +++ b/linux/assets/file-icons/icons/files/astro.svg @@ -0,0 +1,7 @@ + + + + + + A + diff --git a/linux/assets/file-icons/icons/files/audio.svg b/linux/assets/file-icons/icons/files/audio.svg new file mode 100644 index 000000000..20e064dfe --- /dev/null +++ b/linux/assets/file-icons/icons/files/audio.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/authors.svg b/linux/assets/file-icons/icons/files/authors.svg new file mode 100644 index 000000000..c2e044276 --- /dev/null +++ b/linux/assets/file-icons/icons/files/authors.svg @@ -0,0 +1,7 @@ + + + + + + BY + diff --git a/linux/assets/file-icons/icons/files/babel.svg b/linux/assets/file-icons/icons/files/babel.svg new file mode 100644 index 000000000..3d01b0c9c --- /dev/null +++ b/linux/assets/file-icons/icons/files/babel.svg @@ -0,0 +1,7 @@ + + + + + + B + diff --git a/linux/assets/file-icons/icons/files/biome.svg b/linux/assets/file-icons/icons/files/biome.svg new file mode 100644 index 000000000..8a9c038f0 --- /dev/null +++ b/linux/assets/file-icons/icons/files/biome.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/bitbucket.svg b/linux/assets/file-icons/icons/files/bitbucket.svg new file mode 100644 index 000000000..b8cda2c49 --- /dev/null +++ b/linux/assets/file-icons/icons/files/bitbucket.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/blender.svg b/linux/assets/file-icons/icons/files/blender.svg new file mode 100644 index 000000000..55a19331a --- /dev/null +++ b/linux/assets/file-icons/icons/files/blender.svg @@ -0,0 +1,7 @@ + + + + + + B + diff --git a/linux/assets/file-icons/icons/files/bun.svg b/linux/assets/file-icons/icons/files/bun.svg new file mode 100644 index 000000000..633a6c56a --- /dev/null +++ b/linux/assets/file-icons/icons/files/bun.svg @@ -0,0 +1,7 @@ + + + + + + B + diff --git a/linux/assets/file-icons/icons/files/c.svg b/linux/assets/file-icons/icons/files/c.svg new file mode 100644 index 000000000..94161033b --- /dev/null +++ b/linux/assets/file-icons/icons/files/c.svg @@ -0,0 +1,7 @@ + + + + + + C + diff --git a/linux/assets/file-icons/icons/files/certificate.svg b/linux/assets/file-icons/icons/files/certificate.svg new file mode 100644 index 000000000..7d7da3ae5 --- /dev/null +++ b/linux/assets/file-icons/icons/files/certificate.svg @@ -0,0 +1,7 @@ + + + + + + CRT + diff --git a/linux/assets/file-icons/icons/files/changelog.svg b/linux/assets/file-icons/icons/files/changelog.svg new file mode 100644 index 000000000..c9f55d8d9 --- /dev/null +++ b/linux/assets/file-icons/icons/files/changelog.svg @@ -0,0 +1,7 @@ + + + + + + LOG + diff --git a/linux/assets/file-icons/icons/files/claude.svg b/linux/assets/file-icons/icons/files/claude.svg new file mode 100644 index 000000000..59fc7a417 --- /dev/null +++ b/linux/assets/file-icons/icons/files/claude.svg @@ -0,0 +1,7 @@ + + + + + + AI + diff --git a/linux/assets/file-icons/icons/files/cline.svg b/linux/assets/file-icons/icons/files/cline.svg new file mode 100644 index 000000000..ac58f0392 --- /dev/null +++ b/linux/assets/file-icons/icons/files/cline.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/clojure.svg b/linux/assets/file-icons/icons/files/clojure.svg new file mode 100644 index 000000000..506bfd14e --- /dev/null +++ b/linux/assets/file-icons/icons/files/clojure.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/cloudflare.svg b/linux/assets/file-icons/icons/files/cloudflare.svg new file mode 100644 index 000000000..82a406e40 --- /dev/null +++ b/linux/assets/file-icons/icons/files/cloudflare.svg @@ -0,0 +1,7 @@ + + + + + + CF + diff --git a/linux/assets/file-icons/icons/files/cmake.svg b/linux/assets/file-icons/icons/files/cmake.svg new file mode 100644 index 000000000..bf7a4b776 --- /dev/null +++ b/linux/assets/file-icons/icons/files/cmake.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/codex.svg b/linux/assets/file-icons/icons/files/codex.svg new file mode 100644 index 000000000..33de1d910 --- /dev/null +++ b/linux/assets/file-icons/icons/files/codex.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/commitlint.svg b/linux/assets/file-icons/icons/files/commitlint.svg new file mode 100644 index 000000000..00b291e99 --- /dev/null +++ b/linux/assets/file-icons/icons/files/commitlint.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/config.svg b/linux/assets/file-icons/icons/files/config.svg new file mode 100644 index 000000000..8a2de5e52 --- /dev/null +++ b/linux/assets/file-icons/icons/files/config.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/copilot.svg b/linux/assets/file-icons/icons/files/copilot.svg new file mode 100644 index 000000000..e1aef20fb --- /dev/null +++ b/linux/assets/file-icons/icons/files/copilot.svg @@ -0,0 +1,7 @@ + + + + + + AI + diff --git a/linux/assets/file-icons/icons/files/cpp.svg b/linux/assets/file-icons/icons/files/cpp.svg new file mode 100644 index 000000000..ade60b287 --- /dev/null +++ b/linux/assets/file-icons/icons/files/cpp.svg @@ -0,0 +1,7 @@ + + + + + + C+ + diff --git a/linux/assets/file-icons/icons/files/csharp.svg b/linux/assets/file-icons/icons/files/csharp.svg new file mode 100644 index 000000000..15d7d14cd --- /dev/null +++ b/linux/assets/file-icons/icons/files/csharp.svg @@ -0,0 +1,7 @@ + + + + + + C# + diff --git a/linux/assets/file-icons/icons/files/cspell.svg b/linux/assets/file-icons/icons/files/cspell.svg new file mode 100644 index 000000000..0c710dac0 --- /dev/null +++ b/linux/assets/file-icons/icons/files/cspell.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/css.svg b/linux/assets/file-icons/icons/files/css.svg new file mode 100644 index 000000000..1bafc3494 --- /dev/null +++ b/linux/assets/file-icons/icons/files/css.svg @@ -0,0 +1,7 @@ + + + + + + # + diff --git a/linux/assets/file-icons/icons/files/csv.svg b/linux/assets/file-icons/icons/files/csv.svg new file mode 100644 index 000000000..00ff4d9c8 --- /dev/null +++ b/linux/assets/file-icons/icons/files/csv.svg @@ -0,0 +1,7 @@ + + + + + + CSV + diff --git a/linux/assets/file-icons/icons/files/cursor.svg b/linux/assets/file-icons/icons/files/cursor.svg new file mode 100644 index 000000000..0a0383480 --- /dev/null +++ b/linux/assets/file-icons/icons/files/cursor.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/cypress.svg b/linux/assets/file-icons/icons/files/cypress.svg new file mode 100644 index 000000000..3dd11b61d --- /dev/null +++ b/linux/assets/file-icons/icons/files/cypress.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/dart.svg b/linux/assets/file-icons/icons/files/dart.svg new file mode 100644 index 000000000..c326fc377 --- /dev/null +++ b/linux/assets/file-icons/icons/files/dart.svg @@ -0,0 +1,7 @@ + + + + + + D + diff --git a/linux/assets/file-icons/icons/files/database.svg b/linux/assets/file-icons/icons/files/database.svg new file mode 100644 index 000000000..e313be47d --- /dev/null +++ b/linux/assets/file-icons/icons/files/database.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/deno.svg b/linux/assets/file-icons/icons/files/deno.svg new file mode 100644 index 000000000..d9f5b39d7 --- /dev/null +++ b/linux/assets/file-icons/icons/files/deno.svg @@ -0,0 +1,7 @@ + + + + + + D + diff --git a/linux/assets/file-icons/icons/files/dependabot.svg b/linux/assets/file-icons/icons/files/dependabot.svg new file mode 100644 index 000000000..2fac961c6 --- /dev/null +++ b/linux/assets/file-icons/icons/files/dependabot.svg @@ -0,0 +1,7 @@ + + + + + + D + diff --git a/linux/assets/file-icons/icons/files/devcontainer.svg b/linux/assets/file-icons/icons/files/devcontainer.svg new file mode 100644 index 000000000..d927b1b63 --- /dev/null +++ b/linux/assets/file-icons/icons/files/devcontainer.svg @@ -0,0 +1,7 @@ + + + + + + DC + diff --git a/linux/assets/file-icons/icons/files/diff.svg b/linux/assets/file-icons/icons/files/diff.svg new file mode 100644 index 000000000..fbbadc24d --- /dev/null +++ b/linux/assets/file-icons/icons/files/diff.svg @@ -0,0 +1,7 @@ + + + + + + +- + diff --git a/linux/assets/file-icons/icons/files/django.svg b/linux/assets/file-icons/icons/files/django.svg new file mode 100644 index 000000000..29e0d32fc --- /dev/null +++ b/linux/assets/file-icons/icons/files/django.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/docker-compose.svg b/linux/assets/file-icons/icons/files/docker-compose.svg new file mode 100644 index 000000000..68c63db31 --- /dev/null +++ b/linux/assets/file-icons/icons/files/docker-compose.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/docker.svg b/linux/assets/file-icons/icons/files/docker.svg new file mode 100644 index 000000000..9444a3892 --- /dev/null +++ b/linux/assets/file-icons/icons/files/docker.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/document.svg b/linux/assets/file-icons/icons/files/document.svg new file mode 100644 index 000000000..09c9f51bf --- /dev/null +++ b/linux/assets/file-icons/icons/files/document.svg @@ -0,0 +1,7 @@ + + + + + + DOC + diff --git a/linux/assets/file-icons/icons/files/docusaurus.svg b/linux/assets/file-icons/icons/files/docusaurus.svg new file mode 100644 index 000000000..629f18e33 --- /dev/null +++ b/linux/assets/file-icons/icons/files/docusaurus.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/drawio.svg b/linux/assets/file-icons/icons/files/drawio.svg new file mode 100644 index 000000000..c337cfccd --- /dev/null +++ b/linux/assets/file-icons/icons/files/drawio.svg @@ -0,0 +1,7 @@ + + + + + + D + diff --git a/linux/assets/file-icons/icons/files/drizzle.svg b/linux/assets/file-icons/icons/files/drizzle.svg new file mode 100644 index 000000000..7041d34a5 --- /dev/null +++ b/linux/assets/file-icons/icons/files/drizzle.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/editorconfig.svg b/linux/assets/file-icons/icons/files/editorconfig.svg new file mode 100644 index 000000000..2250697b1 --- /dev/null +++ b/linux/assets/file-icons/icons/files/editorconfig.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/electron.svg b/linux/assets/file-icons/icons/files/electron.svg new file mode 100644 index 000000000..2bfb7ff1d --- /dev/null +++ b/linux/assets/file-icons/icons/files/electron.svg @@ -0,0 +1,7 @@ + + + + + + E + diff --git a/linux/assets/file-icons/icons/files/elixir.svg b/linux/assets/file-icons/icons/files/elixir.svg new file mode 100644 index 000000000..d331e75f3 --- /dev/null +++ b/linux/assets/file-icons/icons/files/elixir.svg @@ -0,0 +1,7 @@ + + + + + + EX + diff --git a/linux/assets/file-icons/icons/files/env.svg b/linux/assets/file-icons/icons/files/env.svg new file mode 100644 index 000000000..305241630 --- /dev/null +++ b/linux/assets/file-icons/icons/files/env.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/erlang.svg b/linux/assets/file-icons/icons/files/erlang.svg new file mode 100644 index 000000000..374a8b1e5 --- /dev/null +++ b/linux/assets/file-icons/icons/files/erlang.svg @@ -0,0 +1,7 @@ + + + + + + ER + diff --git a/linux/assets/file-icons/icons/files/error.svg b/linux/assets/file-icons/icons/files/error.svg new file mode 100644 index 000000000..6d1526ae9 --- /dev/null +++ b/linux/assets/file-icons/icons/files/error.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/eslint.svg b/linux/assets/file-icons/icons/files/eslint.svg new file mode 100644 index 000000000..2031a67f4 --- /dev/null +++ b/linux/assets/file-icons/icons/files/eslint.svg @@ -0,0 +1,7 @@ + + + + + + E + diff --git a/linux/assets/file-icons/icons/files/excalidraw.svg b/linux/assets/file-icons/icons/files/excalidraw.svg new file mode 100644 index 000000000..f6c0eeed0 --- /dev/null +++ b/linux/assets/file-icons/icons/files/excalidraw.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/fastapi.svg b/linux/assets/file-icons/icons/files/fastapi.svg new file mode 100644 index 000000000..695f0b01e --- /dev/null +++ b/linux/assets/file-icons/icons/files/fastapi.svg @@ -0,0 +1,7 @@ + + + + + + FA + diff --git a/linux/assets/file-icons/icons/files/figma.svg b/linux/assets/file-icons/icons/files/figma.svg new file mode 100644 index 000000000..dbef799af --- /dev/null +++ b/linux/assets/file-icons/icons/files/figma.svg @@ -0,0 +1,7 @@ + + + + + + F + diff --git a/linux/assets/file-icons/icons/files/file.svg b/linux/assets/file-icons/icons/files/file.svg new file mode 100644 index 000000000..59017652f --- /dev/null +++ b/linux/assets/file-icons/icons/files/file.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/firebase.svg b/linux/assets/file-icons/icons/files/firebase.svg new file mode 100644 index 000000000..c118fb9bf --- /dev/null +++ b/linux/assets/file-icons/icons/files/firebase.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/flask.svg b/linux/assets/file-icons/icons/files/flask.svg new file mode 100644 index 000000000..28bdc7784 --- /dev/null +++ b/linux/assets/file-icons/icons/files/flask.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/font.svg b/linux/assets/file-icons/icons/files/font.svg new file mode 100644 index 000000000..1cc2912bf --- /dev/null +++ b/linux/assets/file-icons/icons/files/font.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/gatsby.svg b/linux/assets/file-icons/icons/files/gatsby.svg new file mode 100644 index 000000000..7a1302efe --- /dev/null +++ b/linux/assets/file-icons/icons/files/gatsby.svg @@ -0,0 +1,7 @@ + + + + + + G + diff --git a/linux/assets/file-icons/icons/files/gemini.svg b/linux/assets/file-icons/icons/files/gemini.svg new file mode 100644 index 000000000..59aea00b6 --- /dev/null +++ b/linux/assets/file-icons/icons/files/gemini.svg @@ -0,0 +1,7 @@ + + + + + + G + diff --git a/linux/assets/file-icons/icons/files/git.svg b/linux/assets/file-icons/icons/files/git.svg new file mode 100644 index 000000000..e5cd60b77 --- /dev/null +++ b/linux/assets/file-icons/icons/files/git.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/github-actions.svg b/linux/assets/file-icons/icons/files/github-actions.svg new file mode 100644 index 000000000..aab3f321d --- /dev/null +++ b/linux/assets/file-icons/icons/files/github-actions.svg @@ -0,0 +1,7 @@ + + + + + + GH + diff --git a/linux/assets/file-icons/icons/files/github.svg b/linux/assets/file-icons/icons/files/github.svg new file mode 100644 index 000000000..8a1f67d7b --- /dev/null +++ b/linux/assets/file-icons/icons/files/github.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/gitlab.svg b/linux/assets/file-icons/icons/files/gitlab.svg new file mode 100644 index 000000000..2500a36ae --- /dev/null +++ b/linux/assets/file-icons/icons/files/gitlab.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/go.svg b/linux/assets/file-icons/icons/files/go.svg new file mode 100644 index 000000000..0174b5232 --- /dev/null +++ b/linux/assets/file-icons/icons/files/go.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/gradle.svg b/linux/assets/file-icons/icons/files/gradle.svg new file mode 100644 index 000000000..267a55ee3 --- /dev/null +++ b/linux/assets/file-icons/icons/files/gradle.svg @@ -0,0 +1,7 @@ + + + + + + G + diff --git a/linux/assets/file-icons/icons/files/graphql-schema.svg b/linux/assets/file-icons/icons/files/graphql-schema.svg new file mode 100644 index 000000000..64f8c0f84 --- /dev/null +++ b/linux/assets/file-icons/icons/files/graphql-schema.svg @@ -0,0 +1,7 @@ + + + + + + GS + diff --git a/linux/assets/file-icons/icons/files/graphql.svg b/linux/assets/file-icons/icons/files/graphql.svg new file mode 100644 index 000000000..bab772115 --- /dev/null +++ b/linux/assets/file-icons/icons/files/graphql.svg @@ -0,0 +1,7 @@ + + + + + + G + diff --git a/linux/assets/file-icons/icons/files/haskell.svg b/linux/assets/file-icons/icons/files/haskell.svg new file mode 100644 index 000000000..52ddcaeee --- /dev/null +++ b/linux/assets/file-icons/icons/files/haskell.svg @@ -0,0 +1,7 @@ + + + + + + HS + diff --git a/linux/assets/file-icons/icons/files/helm.svg b/linux/assets/file-icons/icons/files/helm.svg new file mode 100644 index 000000000..5be27ed28 --- /dev/null +++ b/linux/assets/file-icons/icons/files/helm.svg @@ -0,0 +1,7 @@ + + + + + + H + diff --git a/linux/assets/file-icons/icons/files/html.svg b/linux/assets/file-icons/icons/files/html.svg new file mode 100644 index 000000000..c80ca7a79 --- /dev/null +++ b/linux/assets/file-icons/icons/files/html.svg @@ -0,0 +1,7 @@ + + + + + + H + diff --git a/linux/assets/file-icons/icons/files/http.svg b/linux/assets/file-icons/icons/files/http.svg new file mode 100644 index 000000000..5280b368d --- /dev/null +++ b/linux/assets/file-icons/icons/files/http.svg @@ -0,0 +1,7 @@ + + + + + + HT + diff --git a/linux/assets/file-icons/icons/files/hurl.svg b/linux/assets/file-icons/icons/files/hurl.svg new file mode 100644 index 000000000..bf70415fa --- /dev/null +++ b/linux/assets/file-icons/icons/files/hurl.svg @@ -0,0 +1,7 @@ + + + + + + HU + diff --git a/linux/assets/file-icons/icons/files/image.svg b/linux/assets/file-icons/icons/files/image.svg new file mode 100644 index 000000000..1d2bb2639 --- /dev/null +++ b/linux/assets/file-icons/icons/files/image.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/index-js.svg b/linux/assets/file-icons/icons/files/index-js.svg new file mode 100644 index 000000000..ec34e3867 --- /dev/null +++ b/linux/assets/file-icons/icons/files/index-js.svg @@ -0,0 +1,7 @@ + + + + + + IDX + diff --git a/linux/assets/file-icons/icons/files/index-ts.svg b/linux/assets/file-icons/icons/files/index-ts.svg new file mode 100644 index 000000000..5e532a7ed --- /dev/null +++ b/linux/assets/file-icons/icons/files/index-ts.svg @@ -0,0 +1,7 @@ + + + + + + IDX + diff --git a/linux/assets/file-icons/icons/files/java.svg b/linux/assets/file-icons/icons/files/java.svg new file mode 100644 index 000000000..2bc93d42d --- /dev/null +++ b/linux/assets/file-icons/icons/files/java.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/javascript.svg b/linux/assets/file-icons/icons/files/javascript.svg new file mode 100644 index 000000000..59e3fe6ec --- /dev/null +++ b/linux/assets/file-icons/icons/files/javascript.svg @@ -0,0 +1,7 @@ + + + + + + JS + diff --git a/linux/assets/file-icons/icons/files/jenkins.svg b/linux/assets/file-icons/icons/files/jenkins.svg new file mode 100644 index 000000000..57d712ab0 --- /dev/null +++ b/linux/assets/file-icons/icons/files/jenkins.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/jest.svg b/linux/assets/file-icons/icons/files/jest.svg new file mode 100644 index 000000000..473457b11 --- /dev/null +++ b/linux/assets/file-icons/icons/files/jest.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/jsconfig.svg b/linux/assets/file-icons/icons/files/jsconfig.svg new file mode 100644 index 000000000..ce9fe1edd --- /dev/null +++ b/linux/assets/file-icons/icons/files/jsconfig.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/json.svg b/linux/assets/file-icons/icons/files/json.svg new file mode 100644 index 000000000..943bb57f6 --- /dev/null +++ b/linux/assets/file-icons/icons/files/json.svg @@ -0,0 +1,7 @@ + + + + + + {} + diff --git a/linux/assets/file-icons/icons/files/julia.svg b/linux/assets/file-icons/icons/files/julia.svg new file mode 100644 index 000000000..5a2b777b7 --- /dev/null +++ b/linux/assets/file-icons/icons/files/julia.svg @@ -0,0 +1,7 @@ + + + + + + JL + diff --git a/linux/assets/file-icons/icons/files/key.svg b/linux/assets/file-icons/icons/files/key.svg new file mode 100644 index 000000000..9348cc4ce --- /dev/null +++ b/linux/assets/file-icons/icons/files/key.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/kotlin.svg b/linux/assets/file-icons/icons/files/kotlin.svg new file mode 100644 index 000000000..353a11ce8 --- /dev/null +++ b/linux/assets/file-icons/icons/files/kotlin.svg @@ -0,0 +1,7 @@ + + + + + + K + diff --git a/linux/assets/file-icons/icons/files/kubernetes.svg b/linux/assets/file-icons/icons/files/kubernetes.svg new file mode 100644 index 000000000..9b38b313b --- /dev/null +++ b/linux/assets/file-icons/icons/files/kubernetes.svg @@ -0,0 +1,7 @@ + + + + + + K8 + diff --git a/linux/assets/file-icons/icons/files/laravel.svg b/linux/assets/file-icons/icons/files/laravel.svg new file mode 100644 index 000000000..7be1f3df4 --- /dev/null +++ b/linux/assets/file-icons/icons/files/laravel.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/layout.svg b/linux/assets/file-icons/icons/files/layout.svg new file mode 100644 index 000000000..ccdd1d73f --- /dev/null +++ b/linux/assets/file-icons/icons/files/layout.svg @@ -0,0 +1,7 @@ + + + + + + L + diff --git a/linux/assets/file-icons/icons/files/license.svg b/linux/assets/file-icons/icons/files/license.svg new file mode 100644 index 000000000..053cf8990 --- /dev/null +++ b/linux/assets/file-icons/icons/files/license.svg @@ -0,0 +1,7 @@ + + + + + + LIC + diff --git a/linux/assets/file-icons/icons/files/lintstaged.svg b/linux/assets/file-icons/icons/files/lintstaged.svg new file mode 100644 index 000000000..dcbb7fbce --- /dev/null +++ b/linux/assets/file-icons/icons/files/lintstaged.svg @@ -0,0 +1,7 @@ + + + + + + LS + diff --git a/linux/assets/file-icons/icons/files/linux.svg b/linux/assets/file-icons/icons/files/linux.svg new file mode 100644 index 000000000..b7baaefb1 --- /dev/null +++ b/linux/assets/file-icons/icons/files/linux.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/lit.svg b/linux/assets/file-icons/icons/files/lit.svg new file mode 100644 index 000000000..41956bae5 --- /dev/null +++ b/linux/assets/file-icons/icons/files/lit.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/lithe.svg b/linux/assets/file-icons/icons/files/lithe.svg new file mode 100644 index 000000000..9dd935c7e --- /dev/null +++ b/linux/assets/file-icons/icons/files/lithe.svg @@ -0,0 +1,7 @@ + + + + + + L + diff --git a/linux/assets/file-icons/icons/files/loading.svg b/linux/assets/file-icons/icons/files/loading.svg new file mode 100644 index 000000000..bc3f15ad1 --- /dev/null +++ b/linux/assets/file-icons/icons/files/loading.svg @@ -0,0 +1,7 @@ + + + + + + ... + diff --git a/linux/assets/file-icons/icons/files/lock.svg b/linux/assets/file-icons/icons/files/lock.svg new file mode 100644 index 000000000..278b20f1c --- /dev/null +++ b/linux/assets/file-icons/icons/files/lock.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/log.svg b/linux/assets/file-icons/icons/files/log.svg new file mode 100644 index 000000000..936d5e2a3 --- /dev/null +++ b/linux/assets/file-icons/icons/files/log.svg @@ -0,0 +1,7 @@ + + + + + + LOG + diff --git a/linux/assets/file-icons/icons/files/lua.svg b/linux/assets/file-icons/icons/files/lua.svg new file mode 100644 index 000000000..25ea107b8 --- /dev/null +++ b/linux/assets/file-icons/icons/files/lua.svg @@ -0,0 +1,7 @@ + + + + + + L + diff --git a/linux/assets/file-icons/icons/files/makefile.svg b/linux/assets/file-icons/icons/files/makefile.svg new file mode 100644 index 000000000..a927f9c41 --- /dev/null +++ b/linux/assets/file-icons/icons/files/makefile.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/markdown.svg b/linux/assets/file-icons/icons/files/markdown.svg new file mode 100644 index 000000000..027d5207f --- /dev/null +++ b/linux/assets/file-icons/icons/files/markdown.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/markdownlint.svg b/linux/assets/file-icons/icons/files/markdownlint.svg new file mode 100644 index 000000000..22e601114 --- /dev/null +++ b/linux/assets/file-icons/icons/files/markdownlint.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/maven.svg b/linux/assets/file-icons/icons/files/maven.svg new file mode 100644 index 000000000..dd935da72 --- /dev/null +++ b/linux/assets/file-icons/icons/files/maven.svg @@ -0,0 +1,7 @@ + + + + + + M + diff --git a/linux/assets/file-icons/icons/files/mcp.svg b/linux/assets/file-icons/icons/files/mcp.svg new file mode 100644 index 000000000..963f893fe --- /dev/null +++ b/linux/assets/file-icons/icons/files/mcp.svg @@ -0,0 +1,7 @@ + + + + + + M + diff --git a/linux/assets/file-icons/icons/files/mermaid.svg b/linux/assets/file-icons/icons/files/mermaid.svg new file mode 100644 index 000000000..eb7cab28a --- /dev/null +++ b/linux/assets/file-icons/icons/files/mermaid.svg @@ -0,0 +1,7 @@ + + + + + + MM + diff --git a/linux/assets/file-icons/icons/files/mongo.svg b/linux/assets/file-icons/icons/files/mongo.svg new file mode 100644 index 000000000..96e2a647b --- /dev/null +++ b/linux/assets/file-icons/icons/files/mongo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/netlify.svg b/linux/assets/file-icons/icons/files/netlify.svg new file mode 100644 index 000000000..133725c92 --- /dev/null +++ b/linux/assets/file-icons/icons/files/netlify.svg @@ -0,0 +1,7 @@ + + + + + + N + diff --git a/linux/assets/file-icons/icons/files/next.svg b/linux/assets/file-icons/icons/files/next.svg new file mode 100644 index 000000000..e2d90d83d --- /dev/null +++ b/linux/assets/file-icons/icons/files/next.svg @@ -0,0 +1,7 @@ + + + + + + N + diff --git a/linux/assets/file-icons/icons/files/nginx.svg b/linux/assets/file-icons/icons/files/nginx.svg new file mode 100644 index 000000000..7f1d606bf --- /dev/null +++ b/linux/assets/file-icons/icons/files/nginx.svg @@ -0,0 +1,7 @@ + + + + + + N + diff --git a/linux/assets/file-icons/icons/files/nim.svg b/linux/assets/file-icons/icons/files/nim.svg new file mode 100644 index 000000000..ac80cf1a1 --- /dev/null +++ b/linux/assets/file-icons/icons/files/nim.svg @@ -0,0 +1,7 @@ + + + + + + N + diff --git a/linux/assets/file-icons/icons/files/nix.svg b/linux/assets/file-icons/icons/files/nix.svg new file mode 100644 index 000000000..f8027026f --- /dev/null +++ b/linux/assets/file-icons/icons/files/nix.svg @@ -0,0 +1,7 @@ + + + + + + N + diff --git a/linux/assets/file-icons/icons/files/node.svg b/linux/assets/file-icons/icons/files/node.svg new file mode 100644 index 000000000..aaf878edc --- /dev/null +++ b/linux/assets/file-icons/icons/files/node.svg @@ -0,0 +1,7 @@ + + + + + + N + diff --git a/linux/assets/file-icons/icons/files/not-found.svg b/linux/assets/file-icons/icons/files/not-found.svg new file mode 100644 index 000000000..b0ef19fe1 --- /dev/null +++ b/linux/assets/file-icons/icons/files/not-found.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/notebook.svg b/linux/assets/file-icons/icons/files/notebook.svg new file mode 100644 index 000000000..3c841c9e8 --- /dev/null +++ b/linux/assets/file-icons/icons/files/notebook.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/npm.svg b/linux/assets/file-icons/icons/files/npm.svg new file mode 100644 index 000000000..26c322283 --- /dev/null +++ b/linux/assets/file-icons/icons/files/npm.svg @@ -0,0 +1,7 @@ + + + + + + N + diff --git a/linux/assets/file-icons/icons/files/nuxt.svg b/linux/assets/file-icons/icons/files/nuxt.svg new file mode 100644 index 000000000..fb699c2a2 --- /dev/null +++ b/linux/assets/file-icons/icons/files/nuxt.svg @@ -0,0 +1,7 @@ + + + + + + N + diff --git a/linux/assets/file-icons/icons/files/nx.svg b/linux/assets/file-icons/icons/files/nx.svg new file mode 100644 index 000000000..ef0c748d8 --- /dev/null +++ b/linux/assets/file-icons/icons/files/nx.svg @@ -0,0 +1,7 @@ + + + + + + NX + diff --git a/linux/assets/file-icons/icons/files/ocaml.svg b/linux/assets/file-icons/icons/files/ocaml.svg new file mode 100644 index 000000000..02d552768 --- /dev/null +++ b/linux/assets/file-icons/icons/files/ocaml.svg @@ -0,0 +1,7 @@ + + + + + + ML + diff --git a/linux/assets/file-icons/icons/files/package.svg b/linux/assets/file-icons/icons/files/package.svg new file mode 100644 index 000000000..51f25f450 --- /dev/null +++ b/linux/assets/file-icons/icons/files/package.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/page.svg b/linux/assets/file-icons/icons/files/page.svg new file mode 100644 index 000000000..860d4e6b4 --- /dev/null +++ b/linux/assets/file-icons/icons/files/page.svg @@ -0,0 +1,7 @@ + + + + + + P + diff --git a/linux/assets/file-icons/icons/files/patch.svg b/linux/assets/file-icons/icons/files/patch.svg new file mode 100644 index 000000000..ddaa0c327 --- /dev/null +++ b/linux/assets/file-icons/icons/files/patch.svg @@ -0,0 +1,7 @@ + + + + + + +- + diff --git a/linux/assets/file-icons/icons/files/pdf.svg b/linux/assets/file-icons/icons/files/pdf.svg new file mode 100644 index 000000000..15236fe62 --- /dev/null +++ b/linux/assets/file-icons/icons/files/pdf.svg @@ -0,0 +1,7 @@ + + + + + + PDF + diff --git a/linux/assets/file-icons/icons/files/perl.svg b/linux/assets/file-icons/icons/files/perl.svg new file mode 100644 index 000000000..6e5a983ae --- /dev/null +++ b/linux/assets/file-icons/icons/files/perl.svg @@ -0,0 +1,7 @@ + + + + + + PL + diff --git a/linux/assets/file-icons/icons/files/php.svg b/linux/assets/file-icons/icons/files/php.svg new file mode 100644 index 000000000..a40c8ff3c --- /dev/null +++ b/linux/assets/file-icons/icons/files/php.svg @@ -0,0 +1,7 @@ + + + + + + P + diff --git a/linux/assets/file-icons/icons/files/playwright.svg b/linux/assets/file-icons/icons/files/playwright.svg new file mode 100644 index 000000000..68522e996 --- /dev/null +++ b/linux/assets/file-icons/icons/files/playwright.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/pnpm.svg b/linux/assets/file-icons/icons/files/pnpm.svg new file mode 100644 index 000000000..fcc30d19e --- /dev/null +++ b/linux/assets/file-icons/icons/files/pnpm.svg @@ -0,0 +1,7 @@ + + + + + + P + diff --git a/linux/assets/file-icons/icons/files/postgres.svg b/linux/assets/file-icons/icons/files/postgres.svg new file mode 100644 index 000000000..0f5a3a35f --- /dev/null +++ b/linux/assets/file-icons/icons/files/postgres.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/powerpoint.svg b/linux/assets/file-icons/icons/files/powerpoint.svg new file mode 100644 index 000000000..28ff619e1 --- /dev/null +++ b/linux/assets/file-icons/icons/files/powerpoint.svg @@ -0,0 +1,7 @@ + + + + + + PPT + diff --git a/linux/assets/file-icons/icons/files/prettier.svg b/linux/assets/file-icons/icons/files/prettier.svg new file mode 100644 index 000000000..fc6d6e16a --- /dev/null +++ b/linux/assets/file-icons/icons/files/prettier.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/prisma.svg b/linux/assets/file-icons/icons/files/prisma.svg new file mode 100644 index 000000000..2d6f445e2 --- /dev/null +++ b/linux/assets/file-icons/icons/files/prisma.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/proto.svg b/linux/assets/file-icons/icons/files/proto.svg new file mode 100644 index 000000000..e116a7fdd --- /dev/null +++ b/linux/assets/file-icons/icons/files/proto.svg @@ -0,0 +1,7 @@ + + + + + + P + diff --git a/linux/assets/file-icons/icons/files/python.svg b/linux/assets/file-icons/icons/files/python.svg new file mode 100644 index 000000000..2a3ffb94c --- /dev/null +++ b/linux/assets/file-icons/icons/files/python.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/qwik.svg b/linux/assets/file-icons/icons/files/qwik.svg new file mode 100644 index 000000000..fa7b9a5c9 --- /dev/null +++ b/linux/assets/file-icons/icons/files/qwik.svg @@ -0,0 +1,7 @@ + + + + + + Q + diff --git a/linux/assets/file-icons/icons/files/r.svg b/linux/assets/file-icons/icons/files/r.svg new file mode 100644 index 000000000..abf4b93c1 --- /dev/null +++ b/linux/assets/file-icons/icons/files/r.svg @@ -0,0 +1,7 @@ + + + + + + R + diff --git a/linux/assets/file-icons/icons/files/react.svg b/linux/assets/file-icons/icons/files/react.svg new file mode 100644 index 000000000..88525c8e3 --- /dev/null +++ b/linux/assets/file-icons/icons/files/react.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/redis.svg b/linux/assets/file-icons/icons/files/redis.svg new file mode 100644 index 000000000..ef6c2033a --- /dev/null +++ b/linux/assets/file-icons/icons/files/redis.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/remix.svg b/linux/assets/file-icons/icons/files/remix.svg new file mode 100644 index 000000000..e8dbe13ce --- /dev/null +++ b/linux/assets/file-icons/icons/files/remix.svg @@ -0,0 +1,7 @@ + + + + + + R + diff --git a/linux/assets/file-icons/icons/files/renovate.svg b/linux/assets/file-icons/icons/files/renovate.svg new file mode 100644 index 000000000..4690deb44 --- /dev/null +++ b/linux/assets/file-icons/icons/files/renovate.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/rescript.svg b/linux/assets/file-icons/icons/files/rescript.svg new file mode 100644 index 000000000..f2d3b7c61 --- /dev/null +++ b/linux/assets/file-icons/icons/files/rescript.svg @@ -0,0 +1,7 @@ + + + + + + RE + diff --git a/linux/assets/file-icons/icons/files/rollup.svg b/linux/assets/file-icons/icons/files/rollup.svg new file mode 100644 index 000000000..869e62dde --- /dev/null +++ b/linux/assets/file-icons/icons/files/rollup.svg @@ -0,0 +1,7 @@ + + + + + + R + diff --git a/linux/assets/file-icons/icons/files/route.svg b/linux/assets/file-icons/icons/files/route.svg new file mode 100644 index 000000000..5e2a4c710 --- /dev/null +++ b/linux/assets/file-icons/icons/files/route.svg @@ -0,0 +1,7 @@ + + + + + + RT + diff --git a/linux/assets/file-icons/icons/files/rspack.svg b/linux/assets/file-icons/icons/files/rspack.svg new file mode 100644 index 000000000..302fab094 --- /dev/null +++ b/linux/assets/file-icons/icons/files/rspack.svg @@ -0,0 +1,7 @@ + + + + + + R + diff --git a/linux/assets/file-icons/icons/files/ruby.svg b/linux/assets/file-icons/icons/files/ruby.svg new file mode 100644 index 000000000..818e46b2f --- /dev/null +++ b/linux/assets/file-icons/icons/files/ruby.svg @@ -0,0 +1,7 @@ + + + + + + RB + diff --git a/linux/assets/file-icons/icons/files/rust.svg b/linux/assets/file-icons/icons/files/rust.svg new file mode 100644 index 000000000..3b109303d --- /dev/null +++ b/linux/assets/file-icons/icons/files/rust.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/sass.svg b/linux/assets/file-icons/icons/files/sass.svg new file mode 100644 index 000000000..93d0d5417 --- /dev/null +++ b/linux/assets/file-icons/icons/files/sass.svg @@ -0,0 +1,7 @@ + + + + + + S + diff --git a/linux/assets/file-icons/icons/files/scala.svg b/linux/assets/file-icons/icons/files/scala.svg new file mode 100644 index 000000000..2899b8ef7 --- /dev/null +++ b/linux/assets/file-icons/icons/files/scala.svg @@ -0,0 +1,7 @@ + + + + + + S + diff --git a/linux/assets/file-icons/icons/files/security.svg b/linux/assets/file-icons/icons/files/security.svg new file mode 100644 index 000000000..e8cfb3b5d --- /dev/null +++ b/linux/assets/file-icons/icons/files/security.svg @@ -0,0 +1,7 @@ + + + + + + SEC + diff --git a/linux/assets/file-icons/icons/files/shell.svg b/linux/assets/file-icons/icons/files/shell.svg new file mode 100644 index 000000000..95aecf385 --- /dev/null +++ b/linux/assets/file-icons/icons/files/shell.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/sketch.svg b/linux/assets/file-icons/icons/files/sketch.svg new file mode 100644 index 000000000..1fcb9ba0b --- /dev/null +++ b/linux/assets/file-icons/icons/files/sketch.svg @@ -0,0 +1,7 @@ + + + + + + S + diff --git a/linux/assets/file-icons/icons/files/solid.svg b/linux/assets/file-icons/icons/files/solid.svg new file mode 100644 index 000000000..1eb34aeef --- /dev/null +++ b/linux/assets/file-icons/icons/files/solid.svg @@ -0,0 +1,7 @@ + + + + + + S + diff --git a/linux/assets/file-icons/icons/files/solidity.svg b/linux/assets/file-icons/icons/files/solidity.svg new file mode 100644 index 000000000..1a9a884d8 --- /dev/null +++ b/linux/assets/file-icons/icons/files/solidity.svg @@ -0,0 +1,7 @@ + + + + + + S + diff --git a/linux/assets/file-icons/icons/files/spreadsheet.svg b/linux/assets/file-icons/icons/files/spreadsheet.svg new file mode 100644 index 000000000..591053b33 --- /dev/null +++ b/linux/assets/file-icons/icons/files/spreadsheet.svg @@ -0,0 +1,7 @@ + + + + + + XLS + diff --git a/linux/assets/file-icons/icons/files/sql.svg b/linux/assets/file-icons/icons/files/sql.svg new file mode 100644 index 000000000..886f5f0e8 --- /dev/null +++ b/linux/assets/file-icons/icons/files/sql.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/storybook.svg b/linux/assets/file-icons/icons/files/storybook.svg new file mode 100644 index 000000000..508b0d4b5 --- /dev/null +++ b/linux/assets/file-icons/icons/files/storybook.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/stylelint.svg b/linux/assets/file-icons/icons/files/stylelint.svg new file mode 100644 index 000000000..ddffe630a --- /dev/null +++ b/linux/assets/file-icons/icons/files/stylelint.svg @@ -0,0 +1,7 @@ + + + + + + SL + diff --git a/linux/assets/file-icons/icons/files/supabase.svg b/linux/assets/file-icons/icons/files/supabase.svg new file mode 100644 index 000000000..7fa1d07fb --- /dev/null +++ b/linux/assets/file-icons/icons/files/supabase.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/svelte.svg b/linux/assets/file-icons/icons/files/svelte.svg new file mode 100644 index 000000000..fd08f3199 --- /dev/null +++ b/linux/assets/file-icons/icons/files/svelte.svg @@ -0,0 +1,7 @@ + + + + + + S + diff --git a/linux/assets/file-icons/icons/files/svg.svg b/linux/assets/file-icons/icons/files/svg.svg new file mode 100644 index 000000000..82fe92ab9 --- /dev/null +++ b/linux/assets/file-icons/icons/files/svg.svg @@ -0,0 +1,7 @@ + + + + + + S + diff --git a/linux/assets/file-icons/icons/files/swc.svg b/linux/assets/file-icons/icons/files/swc.svg new file mode 100644 index 000000000..1a45cc869 --- /dev/null +++ b/linux/assets/file-icons/icons/files/swc.svg @@ -0,0 +1,7 @@ + + + + + + S + diff --git a/linux/assets/file-icons/icons/files/swift.svg b/linux/assets/file-icons/icons/files/swift.svg new file mode 100644 index 000000000..0821c2986 --- /dev/null +++ b/linux/assets/file-icons/icons/files/swift.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/tailwind.svg b/linux/assets/file-icons/icons/files/tailwind.svg new file mode 100644 index 000000000..6142d3e56 --- /dev/null +++ b/linux/assets/file-icons/icons/files/tailwind.svg @@ -0,0 +1,7 @@ + + + + + + T + diff --git a/linux/assets/file-icons/icons/files/tauri.svg b/linux/assets/file-icons/icons/files/tauri.svg new file mode 100644 index 000000000..d98590ba6 --- /dev/null +++ b/linux/assets/file-icons/icons/files/tauri.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/terraform.svg b/linux/assets/file-icons/icons/files/terraform.svg new file mode 100644 index 000000000..f02174afb --- /dev/null +++ b/linux/assets/file-icons/icons/files/terraform.svg @@ -0,0 +1,7 @@ + + + + + + TF + diff --git a/linux/assets/file-icons/icons/files/test.svg b/linux/assets/file-icons/icons/files/test.svg new file mode 100644 index 000000000..640d48ccb --- /dev/null +++ b/linux/assets/file-icons/icons/files/test.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/text.svg b/linux/assets/file-icons/icons/files/text.svg new file mode 100644 index 000000000..ce1557bcb --- /dev/null +++ b/linux/assets/file-icons/icons/files/text.svg @@ -0,0 +1,7 @@ + + + + + + TXT + diff --git a/linux/assets/file-icons/icons/files/toml.svg b/linux/assets/file-icons/icons/files/toml.svg new file mode 100644 index 000000000..690cf1e3b --- /dev/null +++ b/linux/assets/file-icons/icons/files/toml.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/turborepo.svg b/linux/assets/file-icons/icons/files/turborepo.svg new file mode 100644 index 000000000..01c8c146d --- /dev/null +++ b/linux/assets/file-icons/icons/files/turborepo.svg @@ -0,0 +1,7 @@ + + + + + + T + diff --git a/linux/assets/file-icons/icons/files/typescript.svg b/linux/assets/file-icons/icons/files/typescript.svg new file mode 100644 index 000000000..6bb44cce8 --- /dev/null +++ b/linux/assets/file-icons/icons/files/typescript.svg @@ -0,0 +1,7 @@ + + + + + + TS + diff --git a/linux/assets/file-icons/icons/files/vercel-config.svg b/linux/assets/file-icons/icons/files/vercel-config.svg new file mode 100644 index 000000000..520eb3b64 --- /dev/null +++ b/linux/assets/file-icons/icons/files/vercel-config.svg @@ -0,0 +1,7 @@ + + + + + + VC + diff --git a/linux/assets/file-icons/icons/files/vercel.svg b/linux/assets/file-icons/icons/files/vercel.svg new file mode 100644 index 000000000..045058ceb --- /dev/null +++ b/linux/assets/file-icons/icons/files/vercel.svg @@ -0,0 +1,7 @@ + + + + + + V + diff --git a/linux/assets/file-icons/icons/files/video.svg b/linux/assets/file-icons/icons/files/video.svg new file mode 100644 index 000000000..dad213208 --- /dev/null +++ b/linux/assets/file-icons/icons/files/video.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/vite.svg b/linux/assets/file-icons/icons/files/vite.svg new file mode 100644 index 000000000..7101b8e53 --- /dev/null +++ b/linux/assets/file-icons/icons/files/vite.svg @@ -0,0 +1,7 @@ + + + + + + V + diff --git a/linux/assets/file-icons/icons/files/vitest.svg b/linux/assets/file-icons/icons/files/vitest.svg new file mode 100644 index 000000000..3a4824a93 --- /dev/null +++ b/linux/assets/file-icons/icons/files/vitest.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/vue.svg b/linux/assets/file-icons/icons/files/vue.svg new file mode 100644 index 000000000..afddcd1dd --- /dev/null +++ b/linux/assets/file-icons/icons/files/vue.svg @@ -0,0 +1,7 @@ + + + + + + V + diff --git a/linux/assets/file-icons/icons/files/warning.svg b/linux/assets/file-icons/icons/files/warning.svg new file mode 100644 index 000000000..576f04b1d --- /dev/null +++ b/linux/assets/file-icons/icons/files/warning.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/wasm.svg b/linux/assets/file-icons/icons/files/wasm.svg new file mode 100644 index 000000000..ed16980f5 --- /dev/null +++ b/linux/assets/file-icons/icons/files/wasm.svg @@ -0,0 +1,7 @@ + + + + + + W + diff --git a/linux/assets/file-icons/icons/files/webpack.svg b/linux/assets/file-icons/icons/files/webpack.svg new file mode 100644 index 000000000..512e1c057 --- /dev/null +++ b/linux/assets/file-icons/icons/files/webpack.svg @@ -0,0 +1,7 @@ + + + + + + W + diff --git a/linux/assets/file-icons/icons/files/windows.svg b/linux/assets/file-icons/icons/files/windows.svg new file mode 100644 index 000000000..dccc1f9bf --- /dev/null +++ b/linux/assets/file-icons/icons/files/windows.svg @@ -0,0 +1,7 @@ + + + + + + W + diff --git a/linux/assets/file-icons/icons/files/word.svg b/linux/assets/file-icons/icons/files/word.svg new file mode 100644 index 000000000..850083eff --- /dev/null +++ b/linux/assets/file-icons/icons/files/word.svg @@ -0,0 +1,7 @@ + + + + + + DOC + diff --git a/linux/assets/file-icons/icons/files/xcode.svg b/linux/assets/file-icons/icons/files/xcode.svg new file mode 100644 index 000000000..9b63fe1cd --- /dev/null +++ b/linux/assets/file-icons/icons/files/xcode.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/files/xml.svg b/linux/assets/file-icons/icons/files/xml.svg new file mode 100644 index 000000000..7905a6bdf --- /dev/null +++ b/linux/assets/file-icons/icons/files/xml.svg @@ -0,0 +1,7 @@ + + + + + + <> + diff --git a/linux/assets/file-icons/icons/files/yaml.svg b/linux/assets/file-icons/icons/files/yaml.svg new file mode 100644 index 000000000..262b00903 --- /dev/null +++ b/linux/assets/file-icons/icons/files/yaml.svg @@ -0,0 +1,7 @@ + + + + + + Y + diff --git a/linux/assets/file-icons/icons/files/yarn.svg b/linux/assets/file-icons/icons/files/yarn.svg new file mode 100644 index 000000000..9ee784fc6 --- /dev/null +++ b/linux/assets/file-icons/icons/files/yarn.svg @@ -0,0 +1,7 @@ + + + + + + Y + diff --git a/linux/assets/file-icons/icons/files/zig.svg b/linux/assets/file-icons/icons/files/zig.svg new file mode 100644 index 000000000..613c135a4 --- /dev/null +++ b/linux/assets/file-icons/icons/files/zig.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-ai-open.svg b/linux/assets/file-icons/icons/folders/folder-ai-open.svg new file mode 100644 index 000000000..c002af7f1 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-ai-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-ai.svg b/linux/assets/file-icons/icons/folders/folder-ai.svg new file mode 100644 index 000000000..c2a7d27f1 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-ai.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-assets-open.svg b/linux/assets/file-icons/icons/folders/folder-assets-open.svg new file mode 100644 index 000000000..07c3cc1c8 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-assets-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-assets.svg b/linux/assets/file-icons/icons/folders/folder-assets.svg new file mode 100644 index 000000000..a4505fc3f --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-assets.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-build-open.svg b/linux/assets/file-icons/icons/folders/folder-build-open.svg new file mode 100644 index 000000000..677645f72 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-build-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-build.svg b/linux/assets/file-icons/icons/folders/folder-build.svg new file mode 100644 index 000000000..f8ae479c8 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-build.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-cloud-open.svg b/linux/assets/file-icons/icons/folders/folder-cloud-open.svg new file mode 100644 index 000000000..93e712555 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-cloud-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-cloud.svg b/linux/assets/file-icons/icons/folders/folder-cloud.svg new file mode 100644 index 000000000..fd76ff9e7 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-cloud.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-components-open.svg b/linux/assets/file-icons/icons/folders/folder-components-open.svg new file mode 100644 index 000000000..eaf07554e --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-components-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-components.svg b/linux/assets/file-icons/icons/folders/folder-components.svg new file mode 100644 index 000000000..9375049e5 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-components.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-config-open.svg b/linux/assets/file-icons/icons/folders/folder-config-open.svg new file mode 100644 index 000000000..433c9e0e9 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-config-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-config.svg b/linux/assets/file-icons/icons/folders/folder-config.svg new file mode 100644 index 000000000..68e7304c2 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-config.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-database-open.svg b/linux/assets/file-icons/icons/folders/folder-database-open.svg new file mode 100644 index 000000000..dcd2fd81a --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-database-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-database.svg b/linux/assets/file-icons/icons/folders/folder-database.svg new file mode 100644 index 000000000..0ed82fe06 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-database.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-docs-open.svg b/linux/assets/file-icons/icons/folders/folder-docs-open.svg new file mode 100644 index 000000000..15024b33e --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-docs-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-docs.svg b/linux/assets/file-icons/icons/folders/folder-docs.svg new file mode 100644 index 000000000..65acb0762 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-docs.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-extensions-open.svg b/linux/assets/file-icons/icons/folders/folder-extensions-open.svg new file mode 100644 index 000000000..c07f6810c --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-extensions-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-extensions.svg b/linux/assets/file-icons/icons/folders/folder-extensions.svg new file mode 100644 index 000000000..ebcfc4aa0 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-extensions.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-git-open.svg b/linux/assets/file-icons/icons/folders/folder-git-open.svg new file mode 100644 index 000000000..df8eca0c3 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-git-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-git.svg b/linux/assets/file-icons/icons/folders/folder-git.svg new file mode 100644 index 000000000..2e6f82e35 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-git.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-locales-open.svg b/linux/assets/file-icons/icons/folders/folder-locales-open.svg new file mode 100644 index 000000000..ea2f034f8 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-locales-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-locales.svg b/linux/assets/file-icons/icons/folders/folder-locales.svg new file mode 100644 index 000000000..785df402a --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-locales.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-mobile-open.svg b/linux/assets/file-icons/icons/folders/folder-mobile-open.svg new file mode 100644 index 000000000..f1e6fc141 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-mobile-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-mobile.svg b/linux/assets/file-icons/icons/folders/folder-mobile.svg new file mode 100644 index 000000000..6f5dc8122 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-mobile.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-open.svg b/linux/assets/file-icons/icons/folders/folder-open.svg new file mode 100644 index 000000000..44cc2562f --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-open.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-packages-open.svg b/linux/assets/file-icons/icons/folders/folder-packages-open.svg new file mode 100644 index 000000000..372bb7bea --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-packages-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-packages.svg b/linux/assets/file-icons/icons/folders/folder-packages.svg new file mode 100644 index 000000000..c60c0c158 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-packages.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-routes-open.svg b/linux/assets/file-icons/icons/folders/folder-routes-open.svg new file mode 100644 index 000000000..e11a12e42 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-routes-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-routes.svg b/linux/assets/file-icons/icons/folders/folder-routes.svg new file mode 100644 index 000000000..ca413527a --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-routes.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-rust-open.svg b/linux/assets/file-icons/icons/folders/folder-rust-open.svg new file mode 100644 index 000000000..a58f44340 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-rust-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-rust.svg b/linux/assets/file-icons/icons/folders/folder-rust.svg new file mode 100644 index 000000000..b4d7ad475 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-rust.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-scripts-open.svg b/linux/assets/file-icons/icons/folders/folder-scripts-open.svg new file mode 100644 index 000000000..48be14c05 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-scripts-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-scripts.svg b/linux/assets/file-icons/icons/folders/folder-scripts.svg new file mode 100644 index 000000000..af0eae3b8 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-scripts.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-security-open.svg b/linux/assets/file-icons/icons/folders/folder-security-open.svg new file mode 100644 index 000000000..f440c6c94 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-security-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-security.svg b/linux/assets/file-icons/icons/folders/folder-security.svg new file mode 100644 index 000000000..7f48358f2 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-security.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-source-open.svg b/linux/assets/file-icons/icons/folders/folder-source-open.svg new file mode 100644 index 000000000..4a3ed1046 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-source-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-source.svg b/linux/assets/file-icons/icons/folders/folder-source.svg new file mode 100644 index 000000000..88bae64e8 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-source.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-styles-open.svg b/linux/assets/file-icons/icons/folders/folder-styles-open.svg new file mode 100644 index 000000000..54fb0735a --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-styles-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-styles.svg b/linux/assets/file-icons/icons/folders/folder-styles.svg new file mode 100644 index 000000000..3bc647e83 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-styles.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-test-open.svg b/linux/assets/file-icons/icons/folders/folder-test-open.svg new file mode 100644 index 000000000..590550958 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-test-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder-test.svg b/linux/assets/file-icons/icons/folders/folder-test.svg new file mode 100644 index 000000000..1de3ba775 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder-test.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/folders/folder.svg b/linux/assets/file-icons/icons/folders/folder.svg new file mode 100644 index 000000000..6e0864bb3 --- /dev/null +++ b/linux/assets/file-icons/icons/folders/folder.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/light/files/adobe.svg b/linux/assets/file-icons/icons/light/files/adobe.svg new file mode 100644 index 000000000..ba716eb30 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/adobe.svg @@ -0,0 +1,7 @@ + + + + + + A + diff --git a/linux/assets/file-icons/icons/light/files/agents.svg b/linux/assets/file-icons/icons/light/files/agents.svg new file mode 100644 index 000000000..e787c0597 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/agents.svg @@ -0,0 +1,7 @@ + + + + + + AI + diff --git a/linux/assets/file-icons/icons/light/files/android.svg b/linux/assets/file-icons/icons/light/files/android.svg new file mode 100644 index 000000000..f30f85a54 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/android.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/angular.svg b/linux/assets/file-icons/icons/light/files/angular.svg new file mode 100644 index 000000000..8d0550c61 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/angular.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/ansible.svg b/linux/assets/file-icons/icons/light/files/ansible.svg new file mode 100644 index 000000000..5b3892010 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/ansible.svg @@ -0,0 +1,7 @@ + + + + + + A + diff --git a/linux/assets/file-icons/icons/light/files/apple.svg b/linux/assets/file-icons/icons/light/files/apple.svg new file mode 100644 index 000000000..acba52121 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/apple.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/archive.svg b/linux/assets/file-icons/icons/light/files/archive.svg new file mode 100644 index 000000000..e952d5813 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/archive.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/arduino.svg b/linux/assets/file-icons/icons/light/files/arduino.svg new file mode 100644 index 000000000..08875c147 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/arduino.svg @@ -0,0 +1,7 @@ + + + + + + A + diff --git a/linux/assets/file-icons/icons/light/files/astro.svg b/linux/assets/file-icons/icons/light/files/astro.svg new file mode 100644 index 000000000..19fd05318 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/astro.svg @@ -0,0 +1,7 @@ + + + + + + A + diff --git a/linux/assets/file-icons/icons/light/files/audio.svg b/linux/assets/file-icons/icons/light/files/audio.svg new file mode 100644 index 000000000..96d9360fd --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/audio.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/authors.svg b/linux/assets/file-icons/icons/light/files/authors.svg new file mode 100644 index 000000000..b408e4921 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/authors.svg @@ -0,0 +1,7 @@ + + + + + + BY + diff --git a/linux/assets/file-icons/icons/light/files/babel.svg b/linux/assets/file-icons/icons/light/files/babel.svg new file mode 100644 index 000000000..c3ce53910 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/babel.svg @@ -0,0 +1,7 @@ + + + + + + B + diff --git a/linux/assets/file-icons/icons/light/files/biome.svg b/linux/assets/file-icons/icons/light/files/biome.svg new file mode 100644 index 000000000..7c80ba33b --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/biome.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/bitbucket.svg b/linux/assets/file-icons/icons/light/files/bitbucket.svg new file mode 100644 index 000000000..7c32ee162 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/bitbucket.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/blender.svg b/linux/assets/file-icons/icons/light/files/blender.svg new file mode 100644 index 000000000..4153cd689 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/blender.svg @@ -0,0 +1,7 @@ + + + + + + B + diff --git a/linux/assets/file-icons/icons/light/files/bun.svg b/linux/assets/file-icons/icons/light/files/bun.svg new file mode 100644 index 000000000..82af13464 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/bun.svg @@ -0,0 +1,7 @@ + + + + + + B + diff --git a/linux/assets/file-icons/icons/light/files/c.svg b/linux/assets/file-icons/icons/light/files/c.svg new file mode 100644 index 000000000..996ae83d7 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/c.svg @@ -0,0 +1,7 @@ + + + + + + C + diff --git a/linux/assets/file-icons/icons/light/files/certificate.svg b/linux/assets/file-icons/icons/light/files/certificate.svg new file mode 100644 index 000000000..1e5abe60f --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/certificate.svg @@ -0,0 +1,7 @@ + + + + + + CRT + diff --git a/linux/assets/file-icons/icons/light/files/changelog.svg b/linux/assets/file-icons/icons/light/files/changelog.svg new file mode 100644 index 000000000..ef01e6c7d --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/changelog.svg @@ -0,0 +1,7 @@ + + + + + + LOG + diff --git a/linux/assets/file-icons/icons/light/files/claude.svg b/linux/assets/file-icons/icons/light/files/claude.svg new file mode 100644 index 000000000..378c9e243 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/claude.svg @@ -0,0 +1,7 @@ + + + + + + AI + diff --git a/linux/assets/file-icons/icons/light/files/cline.svg b/linux/assets/file-icons/icons/light/files/cline.svg new file mode 100644 index 000000000..552832ef4 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/cline.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/clojure.svg b/linux/assets/file-icons/icons/light/files/clojure.svg new file mode 100644 index 000000000..d9521265b --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/clojure.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/cloudflare.svg b/linux/assets/file-icons/icons/light/files/cloudflare.svg new file mode 100644 index 000000000..97d63b54c --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/cloudflare.svg @@ -0,0 +1,7 @@ + + + + + + CF + diff --git a/linux/assets/file-icons/icons/light/files/cmake.svg b/linux/assets/file-icons/icons/light/files/cmake.svg new file mode 100644 index 000000000..cc06991ae --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/cmake.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/codex.svg b/linux/assets/file-icons/icons/light/files/codex.svg new file mode 100644 index 000000000..fd1a89358 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/codex.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/commitlint.svg b/linux/assets/file-icons/icons/light/files/commitlint.svg new file mode 100644 index 000000000..9566b78f6 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/commitlint.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/config.svg b/linux/assets/file-icons/icons/light/files/config.svg new file mode 100644 index 000000000..ab0ecc327 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/config.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/copilot.svg b/linux/assets/file-icons/icons/light/files/copilot.svg new file mode 100644 index 000000000..a7ef00343 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/copilot.svg @@ -0,0 +1,7 @@ + + + + + + AI + diff --git a/linux/assets/file-icons/icons/light/files/cpp.svg b/linux/assets/file-icons/icons/light/files/cpp.svg new file mode 100644 index 000000000..b71ea0cad --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/cpp.svg @@ -0,0 +1,7 @@ + + + + + + C+ + diff --git a/linux/assets/file-icons/icons/light/files/csharp.svg b/linux/assets/file-icons/icons/light/files/csharp.svg new file mode 100644 index 000000000..2f49b3ea6 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/csharp.svg @@ -0,0 +1,7 @@ + + + + + + C# + diff --git a/linux/assets/file-icons/icons/light/files/cspell.svg b/linux/assets/file-icons/icons/light/files/cspell.svg new file mode 100644 index 000000000..1d7e55469 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/cspell.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/css.svg b/linux/assets/file-icons/icons/light/files/css.svg new file mode 100644 index 000000000..375de6653 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/css.svg @@ -0,0 +1,7 @@ + + + + + + # + diff --git a/linux/assets/file-icons/icons/light/files/csv.svg b/linux/assets/file-icons/icons/light/files/csv.svg new file mode 100644 index 000000000..7ed227c51 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/csv.svg @@ -0,0 +1,7 @@ + + + + + + CSV + diff --git a/linux/assets/file-icons/icons/light/files/cursor.svg b/linux/assets/file-icons/icons/light/files/cursor.svg new file mode 100644 index 000000000..df8003f2e --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/cursor.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/cypress.svg b/linux/assets/file-icons/icons/light/files/cypress.svg new file mode 100644 index 000000000..34e832521 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/cypress.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/dart.svg b/linux/assets/file-icons/icons/light/files/dart.svg new file mode 100644 index 000000000..941437104 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/dart.svg @@ -0,0 +1,7 @@ + + + + + + D + diff --git a/linux/assets/file-icons/icons/light/files/database.svg b/linux/assets/file-icons/icons/light/files/database.svg new file mode 100644 index 000000000..2040e77f0 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/database.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/deno.svg b/linux/assets/file-icons/icons/light/files/deno.svg new file mode 100644 index 000000000..301830fce --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/deno.svg @@ -0,0 +1,7 @@ + + + + + + D + diff --git a/linux/assets/file-icons/icons/light/files/dependabot.svg b/linux/assets/file-icons/icons/light/files/dependabot.svg new file mode 100644 index 000000000..38a401d0e --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/dependabot.svg @@ -0,0 +1,7 @@ + + + + + + D + diff --git a/linux/assets/file-icons/icons/light/files/devcontainer.svg b/linux/assets/file-icons/icons/light/files/devcontainer.svg new file mode 100644 index 000000000..9f5dd2277 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/devcontainer.svg @@ -0,0 +1,7 @@ + + + + + + DC + diff --git a/linux/assets/file-icons/icons/light/files/diff.svg b/linux/assets/file-icons/icons/light/files/diff.svg new file mode 100644 index 000000000..6f52d90e4 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/diff.svg @@ -0,0 +1,7 @@ + + + + + + +- + diff --git a/linux/assets/file-icons/icons/light/files/django.svg b/linux/assets/file-icons/icons/light/files/django.svg new file mode 100644 index 000000000..4ec56bcfb --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/django.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/docker-compose.svg b/linux/assets/file-icons/icons/light/files/docker-compose.svg new file mode 100644 index 000000000..2d4c58925 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/docker-compose.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/docker.svg b/linux/assets/file-icons/icons/light/files/docker.svg new file mode 100644 index 000000000..867766922 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/docker.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/document.svg b/linux/assets/file-icons/icons/light/files/document.svg new file mode 100644 index 000000000..f5ad58865 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/document.svg @@ -0,0 +1,7 @@ + + + + + + DOC + diff --git a/linux/assets/file-icons/icons/light/files/docusaurus.svg b/linux/assets/file-icons/icons/light/files/docusaurus.svg new file mode 100644 index 000000000..ed0fe2214 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/docusaurus.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/drawio.svg b/linux/assets/file-icons/icons/light/files/drawio.svg new file mode 100644 index 000000000..807b2aa58 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/drawio.svg @@ -0,0 +1,7 @@ + + + + + + D + diff --git a/linux/assets/file-icons/icons/light/files/drizzle.svg b/linux/assets/file-icons/icons/light/files/drizzle.svg new file mode 100644 index 000000000..29a554e29 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/drizzle.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/editorconfig.svg b/linux/assets/file-icons/icons/light/files/editorconfig.svg new file mode 100644 index 000000000..2532cefd4 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/editorconfig.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/electron.svg b/linux/assets/file-icons/icons/light/files/electron.svg new file mode 100644 index 000000000..066bddd15 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/electron.svg @@ -0,0 +1,7 @@ + + + + + + E + diff --git a/linux/assets/file-icons/icons/light/files/elixir.svg b/linux/assets/file-icons/icons/light/files/elixir.svg new file mode 100644 index 000000000..70e88edc3 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/elixir.svg @@ -0,0 +1,7 @@ + + + + + + EX + diff --git a/linux/assets/file-icons/icons/light/files/env.svg b/linux/assets/file-icons/icons/light/files/env.svg new file mode 100644 index 000000000..739868f76 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/env.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/erlang.svg b/linux/assets/file-icons/icons/light/files/erlang.svg new file mode 100644 index 000000000..6dd8f7210 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/erlang.svg @@ -0,0 +1,7 @@ + + + + + + ER + diff --git a/linux/assets/file-icons/icons/light/files/error.svg b/linux/assets/file-icons/icons/light/files/error.svg new file mode 100644 index 000000000..6777185ab --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/error.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/eslint.svg b/linux/assets/file-icons/icons/light/files/eslint.svg new file mode 100644 index 000000000..31cdc71f1 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/eslint.svg @@ -0,0 +1,7 @@ + + + + + + E + diff --git a/linux/assets/file-icons/icons/light/files/excalidraw.svg b/linux/assets/file-icons/icons/light/files/excalidraw.svg new file mode 100644 index 000000000..b3ab80975 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/excalidraw.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/fastapi.svg b/linux/assets/file-icons/icons/light/files/fastapi.svg new file mode 100644 index 000000000..cf4cb4448 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/fastapi.svg @@ -0,0 +1,7 @@ + + + + + + FA + diff --git a/linux/assets/file-icons/icons/light/files/figma.svg b/linux/assets/file-icons/icons/light/files/figma.svg new file mode 100644 index 000000000..9d3f20dfc --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/figma.svg @@ -0,0 +1,7 @@ + + + + + + F + diff --git a/linux/assets/file-icons/icons/light/files/file.svg b/linux/assets/file-icons/icons/light/files/file.svg new file mode 100644 index 000000000..ecd9c5967 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/file.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/firebase.svg b/linux/assets/file-icons/icons/light/files/firebase.svg new file mode 100644 index 000000000..160cd7a47 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/firebase.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/flask.svg b/linux/assets/file-icons/icons/light/files/flask.svg new file mode 100644 index 000000000..f91b6630f --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/flask.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/font.svg b/linux/assets/file-icons/icons/light/files/font.svg new file mode 100644 index 000000000..232442e9e --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/font.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/gatsby.svg b/linux/assets/file-icons/icons/light/files/gatsby.svg new file mode 100644 index 000000000..f0ccaaf81 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/gatsby.svg @@ -0,0 +1,7 @@ + + + + + + G + diff --git a/linux/assets/file-icons/icons/light/files/gemini.svg b/linux/assets/file-icons/icons/light/files/gemini.svg new file mode 100644 index 000000000..aa1b35013 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/gemini.svg @@ -0,0 +1,7 @@ + + + + + + G + diff --git a/linux/assets/file-icons/icons/light/files/git.svg b/linux/assets/file-icons/icons/light/files/git.svg new file mode 100644 index 000000000..60e5c97fe --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/git.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/github-actions.svg b/linux/assets/file-icons/icons/light/files/github-actions.svg new file mode 100644 index 000000000..1a8595f25 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/github-actions.svg @@ -0,0 +1,7 @@ + + + + + + GH + diff --git a/linux/assets/file-icons/icons/light/files/github.svg b/linux/assets/file-icons/icons/light/files/github.svg new file mode 100644 index 000000000..ac94fb622 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/github.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/gitlab.svg b/linux/assets/file-icons/icons/light/files/gitlab.svg new file mode 100644 index 000000000..eb6eb35ed --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/gitlab.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/go.svg b/linux/assets/file-icons/icons/light/files/go.svg new file mode 100644 index 000000000..73f3b11a9 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/go.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/gradle.svg b/linux/assets/file-icons/icons/light/files/gradle.svg new file mode 100644 index 000000000..456555037 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/gradle.svg @@ -0,0 +1,7 @@ + + + + + + G + diff --git a/linux/assets/file-icons/icons/light/files/graphql-schema.svg b/linux/assets/file-icons/icons/light/files/graphql-schema.svg new file mode 100644 index 000000000..d50cccb17 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/graphql-schema.svg @@ -0,0 +1,7 @@ + + + + + + GS + diff --git a/linux/assets/file-icons/icons/light/files/graphql.svg b/linux/assets/file-icons/icons/light/files/graphql.svg new file mode 100644 index 000000000..c63624564 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/graphql.svg @@ -0,0 +1,7 @@ + + + + + + G + diff --git a/linux/assets/file-icons/icons/light/files/haskell.svg b/linux/assets/file-icons/icons/light/files/haskell.svg new file mode 100644 index 000000000..24db0ded5 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/haskell.svg @@ -0,0 +1,7 @@ + + + + + + HS + diff --git a/linux/assets/file-icons/icons/light/files/helm.svg b/linux/assets/file-icons/icons/light/files/helm.svg new file mode 100644 index 000000000..f1c3bb82a --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/helm.svg @@ -0,0 +1,7 @@ + + + + + + H + diff --git a/linux/assets/file-icons/icons/light/files/html.svg b/linux/assets/file-icons/icons/light/files/html.svg new file mode 100644 index 000000000..d1e8d808b --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/html.svg @@ -0,0 +1,7 @@ + + + + + + H + diff --git a/linux/assets/file-icons/icons/light/files/http.svg b/linux/assets/file-icons/icons/light/files/http.svg new file mode 100644 index 000000000..977a063c4 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/http.svg @@ -0,0 +1,7 @@ + + + + + + HT + diff --git a/linux/assets/file-icons/icons/light/files/hurl.svg b/linux/assets/file-icons/icons/light/files/hurl.svg new file mode 100644 index 000000000..1ce145c73 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/hurl.svg @@ -0,0 +1,7 @@ + + + + + + HU + diff --git a/linux/assets/file-icons/icons/light/files/image.svg b/linux/assets/file-icons/icons/light/files/image.svg new file mode 100644 index 000000000..2bfa5eb89 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/image.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/index-js.svg b/linux/assets/file-icons/icons/light/files/index-js.svg new file mode 100644 index 000000000..e676bf67e --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/index-js.svg @@ -0,0 +1,7 @@ + + + + + + IDX + diff --git a/linux/assets/file-icons/icons/light/files/index-ts.svg b/linux/assets/file-icons/icons/light/files/index-ts.svg new file mode 100644 index 000000000..9c62ff182 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/index-ts.svg @@ -0,0 +1,7 @@ + + + + + + IDX + diff --git a/linux/assets/file-icons/icons/light/files/java.svg b/linux/assets/file-icons/icons/light/files/java.svg new file mode 100644 index 000000000..948e01ddd --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/java.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/javascript.svg b/linux/assets/file-icons/icons/light/files/javascript.svg new file mode 100644 index 000000000..0e36f8cfe --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/javascript.svg @@ -0,0 +1,7 @@ + + + + + + JS + diff --git a/linux/assets/file-icons/icons/light/files/jenkins.svg b/linux/assets/file-icons/icons/light/files/jenkins.svg new file mode 100644 index 000000000..cc549fc11 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/jenkins.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/jest.svg b/linux/assets/file-icons/icons/light/files/jest.svg new file mode 100644 index 000000000..97aef2580 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/jest.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/jsconfig.svg b/linux/assets/file-icons/icons/light/files/jsconfig.svg new file mode 100644 index 000000000..2af749c4d --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/jsconfig.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/json.svg b/linux/assets/file-icons/icons/light/files/json.svg new file mode 100644 index 000000000..9bd3910d1 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/json.svg @@ -0,0 +1,7 @@ + + + + + + {} + diff --git a/linux/assets/file-icons/icons/light/files/julia.svg b/linux/assets/file-icons/icons/light/files/julia.svg new file mode 100644 index 000000000..1400b22df --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/julia.svg @@ -0,0 +1,7 @@ + + + + + + JL + diff --git a/linux/assets/file-icons/icons/light/files/key.svg b/linux/assets/file-icons/icons/light/files/key.svg new file mode 100644 index 000000000..48a792e87 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/key.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/kotlin.svg b/linux/assets/file-icons/icons/light/files/kotlin.svg new file mode 100644 index 000000000..8d0fd4dd0 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/kotlin.svg @@ -0,0 +1,7 @@ + + + + + + K + diff --git a/linux/assets/file-icons/icons/light/files/kubernetes.svg b/linux/assets/file-icons/icons/light/files/kubernetes.svg new file mode 100644 index 000000000..6837d0a06 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/kubernetes.svg @@ -0,0 +1,7 @@ + + + + + + K8 + diff --git a/linux/assets/file-icons/icons/light/files/laravel.svg b/linux/assets/file-icons/icons/light/files/laravel.svg new file mode 100644 index 000000000..8c2f3393e --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/laravel.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/layout.svg b/linux/assets/file-icons/icons/light/files/layout.svg new file mode 100644 index 000000000..70c99e58b --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/layout.svg @@ -0,0 +1,7 @@ + + + + + + L + diff --git a/linux/assets/file-icons/icons/light/files/license.svg b/linux/assets/file-icons/icons/light/files/license.svg new file mode 100644 index 000000000..0aa5fbce7 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/license.svg @@ -0,0 +1,7 @@ + + + + + + LIC + diff --git a/linux/assets/file-icons/icons/light/files/lintstaged.svg b/linux/assets/file-icons/icons/light/files/lintstaged.svg new file mode 100644 index 000000000..0caeb0f9f --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/lintstaged.svg @@ -0,0 +1,7 @@ + + + + + + LS + diff --git a/linux/assets/file-icons/icons/light/files/linux.svg b/linux/assets/file-icons/icons/light/files/linux.svg new file mode 100644 index 000000000..b43691eb4 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/linux.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/lit.svg b/linux/assets/file-icons/icons/light/files/lit.svg new file mode 100644 index 000000000..76e58c410 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/lit.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/lithe.svg b/linux/assets/file-icons/icons/light/files/lithe.svg new file mode 100644 index 000000000..dc5523ab4 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/lithe.svg @@ -0,0 +1,7 @@ + + + + + + L + diff --git a/linux/assets/file-icons/icons/light/files/loading.svg b/linux/assets/file-icons/icons/light/files/loading.svg new file mode 100644 index 000000000..cb632d555 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/loading.svg @@ -0,0 +1,7 @@ + + + + + + ... + diff --git a/linux/assets/file-icons/icons/light/files/lock.svg b/linux/assets/file-icons/icons/light/files/lock.svg new file mode 100644 index 000000000..8abab2a1d --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/lock.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/log.svg b/linux/assets/file-icons/icons/light/files/log.svg new file mode 100644 index 000000000..f06880824 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/log.svg @@ -0,0 +1,7 @@ + + + + + + LOG + diff --git a/linux/assets/file-icons/icons/light/files/lua.svg b/linux/assets/file-icons/icons/light/files/lua.svg new file mode 100644 index 000000000..3fc5f6d9b --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/lua.svg @@ -0,0 +1,7 @@ + + + + + + L + diff --git a/linux/assets/file-icons/icons/light/files/makefile.svg b/linux/assets/file-icons/icons/light/files/makefile.svg new file mode 100644 index 000000000..e6ca2fb8b --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/makefile.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/markdown.svg b/linux/assets/file-icons/icons/light/files/markdown.svg new file mode 100644 index 000000000..0a57e1538 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/markdown.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/markdownlint.svg b/linux/assets/file-icons/icons/light/files/markdownlint.svg new file mode 100644 index 000000000..3aabe5da5 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/markdownlint.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/maven.svg b/linux/assets/file-icons/icons/light/files/maven.svg new file mode 100644 index 000000000..b5d240b51 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/maven.svg @@ -0,0 +1,7 @@ + + + + + + M + diff --git a/linux/assets/file-icons/icons/light/files/mcp.svg b/linux/assets/file-icons/icons/light/files/mcp.svg new file mode 100644 index 000000000..9480e0499 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/mcp.svg @@ -0,0 +1,7 @@ + + + + + + M + diff --git a/linux/assets/file-icons/icons/light/files/mermaid.svg b/linux/assets/file-icons/icons/light/files/mermaid.svg new file mode 100644 index 000000000..4d45d6f3c --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/mermaid.svg @@ -0,0 +1,7 @@ + + + + + + MM + diff --git a/linux/assets/file-icons/icons/light/files/mongo.svg b/linux/assets/file-icons/icons/light/files/mongo.svg new file mode 100644 index 000000000..18986c84c --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/mongo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/netlify.svg b/linux/assets/file-icons/icons/light/files/netlify.svg new file mode 100644 index 000000000..941d09eb5 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/netlify.svg @@ -0,0 +1,7 @@ + + + + + + N + diff --git a/linux/assets/file-icons/icons/light/files/next.svg b/linux/assets/file-icons/icons/light/files/next.svg new file mode 100644 index 000000000..6b62626f4 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/next.svg @@ -0,0 +1,7 @@ + + + + + + N + diff --git a/linux/assets/file-icons/icons/light/files/nginx.svg b/linux/assets/file-icons/icons/light/files/nginx.svg new file mode 100644 index 000000000..e56efaeea --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/nginx.svg @@ -0,0 +1,7 @@ + + + + + + N + diff --git a/linux/assets/file-icons/icons/light/files/nim.svg b/linux/assets/file-icons/icons/light/files/nim.svg new file mode 100644 index 000000000..8fb2d6ee4 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/nim.svg @@ -0,0 +1,7 @@ + + + + + + N + diff --git a/linux/assets/file-icons/icons/light/files/nix.svg b/linux/assets/file-icons/icons/light/files/nix.svg new file mode 100644 index 000000000..5e4119b95 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/nix.svg @@ -0,0 +1,7 @@ + + + + + + N + diff --git a/linux/assets/file-icons/icons/light/files/node.svg b/linux/assets/file-icons/icons/light/files/node.svg new file mode 100644 index 000000000..6bc1f64b2 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/node.svg @@ -0,0 +1,7 @@ + + + + + + N + diff --git a/linux/assets/file-icons/icons/light/files/not-found.svg b/linux/assets/file-icons/icons/light/files/not-found.svg new file mode 100644 index 000000000..0062bda71 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/not-found.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/notebook.svg b/linux/assets/file-icons/icons/light/files/notebook.svg new file mode 100644 index 000000000..a9c69987e --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/notebook.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/npm.svg b/linux/assets/file-icons/icons/light/files/npm.svg new file mode 100644 index 000000000..bfb1828ca --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/npm.svg @@ -0,0 +1,7 @@ + + + + + + N + diff --git a/linux/assets/file-icons/icons/light/files/nuxt.svg b/linux/assets/file-icons/icons/light/files/nuxt.svg new file mode 100644 index 000000000..d169e54d3 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/nuxt.svg @@ -0,0 +1,7 @@ + + + + + + N + diff --git a/linux/assets/file-icons/icons/light/files/nx.svg b/linux/assets/file-icons/icons/light/files/nx.svg new file mode 100644 index 000000000..69e48c5c8 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/nx.svg @@ -0,0 +1,7 @@ + + + + + + NX + diff --git a/linux/assets/file-icons/icons/light/files/ocaml.svg b/linux/assets/file-icons/icons/light/files/ocaml.svg new file mode 100644 index 000000000..57d304597 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/ocaml.svg @@ -0,0 +1,7 @@ + + + + + + ML + diff --git a/linux/assets/file-icons/icons/light/files/package.svg b/linux/assets/file-icons/icons/light/files/package.svg new file mode 100644 index 000000000..21909634f --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/package.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/page.svg b/linux/assets/file-icons/icons/light/files/page.svg new file mode 100644 index 000000000..6832c7701 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/page.svg @@ -0,0 +1,7 @@ + + + + + + P + diff --git a/linux/assets/file-icons/icons/light/files/patch.svg b/linux/assets/file-icons/icons/light/files/patch.svg new file mode 100644 index 000000000..5c1c448a2 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/patch.svg @@ -0,0 +1,7 @@ + + + + + + +- + diff --git a/linux/assets/file-icons/icons/light/files/pdf.svg b/linux/assets/file-icons/icons/light/files/pdf.svg new file mode 100644 index 000000000..e79c22e9b --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/pdf.svg @@ -0,0 +1,7 @@ + + + + + + PDF + diff --git a/linux/assets/file-icons/icons/light/files/perl.svg b/linux/assets/file-icons/icons/light/files/perl.svg new file mode 100644 index 000000000..a41c0cacd --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/perl.svg @@ -0,0 +1,7 @@ + + + + + + PL + diff --git a/linux/assets/file-icons/icons/light/files/php.svg b/linux/assets/file-icons/icons/light/files/php.svg new file mode 100644 index 000000000..d4c2edfe9 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/php.svg @@ -0,0 +1,7 @@ + + + + + + P + diff --git a/linux/assets/file-icons/icons/light/files/playwright.svg b/linux/assets/file-icons/icons/light/files/playwright.svg new file mode 100644 index 000000000..0317c2886 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/playwright.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/pnpm.svg b/linux/assets/file-icons/icons/light/files/pnpm.svg new file mode 100644 index 000000000..03d12c964 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/pnpm.svg @@ -0,0 +1,7 @@ + + + + + + P + diff --git a/linux/assets/file-icons/icons/light/files/postgres.svg b/linux/assets/file-icons/icons/light/files/postgres.svg new file mode 100644 index 000000000..368daa77f --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/postgres.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/powerpoint.svg b/linux/assets/file-icons/icons/light/files/powerpoint.svg new file mode 100644 index 000000000..2e8a55782 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/powerpoint.svg @@ -0,0 +1,7 @@ + + + + + + PPT + diff --git a/linux/assets/file-icons/icons/light/files/prettier.svg b/linux/assets/file-icons/icons/light/files/prettier.svg new file mode 100644 index 000000000..8f24fa2de --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/prettier.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/prisma.svg b/linux/assets/file-icons/icons/light/files/prisma.svg new file mode 100644 index 000000000..3a575c6c6 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/prisma.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/proto.svg b/linux/assets/file-icons/icons/light/files/proto.svg new file mode 100644 index 000000000..6551ba995 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/proto.svg @@ -0,0 +1,7 @@ + + + + + + P + diff --git a/linux/assets/file-icons/icons/light/files/python.svg b/linux/assets/file-icons/icons/light/files/python.svg new file mode 100644 index 000000000..72f09abe9 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/python.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/qwik.svg b/linux/assets/file-icons/icons/light/files/qwik.svg new file mode 100644 index 000000000..5e09fe0eb --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/qwik.svg @@ -0,0 +1,7 @@ + + + + + + Q + diff --git a/linux/assets/file-icons/icons/light/files/r.svg b/linux/assets/file-icons/icons/light/files/r.svg new file mode 100644 index 000000000..b496fd493 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/r.svg @@ -0,0 +1,7 @@ + + + + + + R + diff --git a/linux/assets/file-icons/icons/light/files/react.svg b/linux/assets/file-icons/icons/light/files/react.svg new file mode 100644 index 000000000..aac59a6c4 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/react.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/redis.svg b/linux/assets/file-icons/icons/light/files/redis.svg new file mode 100644 index 000000000..7cd4c2f24 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/redis.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/remix.svg b/linux/assets/file-icons/icons/light/files/remix.svg new file mode 100644 index 000000000..975fda0a2 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/remix.svg @@ -0,0 +1,7 @@ + + + + + + R + diff --git a/linux/assets/file-icons/icons/light/files/renovate.svg b/linux/assets/file-icons/icons/light/files/renovate.svg new file mode 100644 index 000000000..a95016811 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/renovate.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/rescript.svg b/linux/assets/file-icons/icons/light/files/rescript.svg new file mode 100644 index 000000000..eb898c1d1 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/rescript.svg @@ -0,0 +1,7 @@ + + + + + + RE + diff --git a/linux/assets/file-icons/icons/light/files/rollup.svg b/linux/assets/file-icons/icons/light/files/rollup.svg new file mode 100644 index 000000000..8c846b294 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/rollup.svg @@ -0,0 +1,7 @@ + + + + + + R + diff --git a/linux/assets/file-icons/icons/light/files/route.svg b/linux/assets/file-icons/icons/light/files/route.svg new file mode 100644 index 000000000..a1db3d411 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/route.svg @@ -0,0 +1,7 @@ + + + + + + RT + diff --git a/linux/assets/file-icons/icons/light/files/rspack.svg b/linux/assets/file-icons/icons/light/files/rspack.svg new file mode 100644 index 000000000..a33654646 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/rspack.svg @@ -0,0 +1,7 @@ + + + + + + R + diff --git a/linux/assets/file-icons/icons/light/files/ruby.svg b/linux/assets/file-icons/icons/light/files/ruby.svg new file mode 100644 index 000000000..d245a2255 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/ruby.svg @@ -0,0 +1,7 @@ + + + + + + RB + diff --git a/linux/assets/file-icons/icons/light/files/rust.svg b/linux/assets/file-icons/icons/light/files/rust.svg new file mode 100644 index 000000000..519da0c0d --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/rust.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/sass.svg b/linux/assets/file-icons/icons/light/files/sass.svg new file mode 100644 index 000000000..753e00605 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/sass.svg @@ -0,0 +1,7 @@ + + + + + + S + diff --git a/linux/assets/file-icons/icons/light/files/scala.svg b/linux/assets/file-icons/icons/light/files/scala.svg new file mode 100644 index 000000000..f90003510 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/scala.svg @@ -0,0 +1,7 @@ + + + + + + S + diff --git a/linux/assets/file-icons/icons/light/files/security.svg b/linux/assets/file-icons/icons/light/files/security.svg new file mode 100644 index 000000000..349ea6213 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/security.svg @@ -0,0 +1,7 @@ + + + + + + SEC + diff --git a/linux/assets/file-icons/icons/light/files/shell.svg b/linux/assets/file-icons/icons/light/files/shell.svg new file mode 100644 index 000000000..90d4ad418 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/shell.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/sketch.svg b/linux/assets/file-icons/icons/light/files/sketch.svg new file mode 100644 index 000000000..740150901 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/sketch.svg @@ -0,0 +1,7 @@ + + + + + + S + diff --git a/linux/assets/file-icons/icons/light/files/solid.svg b/linux/assets/file-icons/icons/light/files/solid.svg new file mode 100644 index 000000000..e0d41430a --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/solid.svg @@ -0,0 +1,7 @@ + + + + + + S + diff --git a/linux/assets/file-icons/icons/light/files/solidity.svg b/linux/assets/file-icons/icons/light/files/solidity.svg new file mode 100644 index 000000000..646c19201 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/solidity.svg @@ -0,0 +1,7 @@ + + + + + + S + diff --git a/linux/assets/file-icons/icons/light/files/spreadsheet.svg b/linux/assets/file-icons/icons/light/files/spreadsheet.svg new file mode 100644 index 000000000..72a4c1e5e --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/spreadsheet.svg @@ -0,0 +1,7 @@ + + + + + + XLS + diff --git a/linux/assets/file-icons/icons/light/files/sql.svg b/linux/assets/file-icons/icons/light/files/sql.svg new file mode 100644 index 000000000..dbaad7bf8 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/sql.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/storybook.svg b/linux/assets/file-icons/icons/light/files/storybook.svg new file mode 100644 index 000000000..23f0d98b4 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/storybook.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/stylelint.svg b/linux/assets/file-icons/icons/light/files/stylelint.svg new file mode 100644 index 000000000..bfca7276e --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/stylelint.svg @@ -0,0 +1,7 @@ + + + + + + SL + diff --git a/linux/assets/file-icons/icons/light/files/supabase.svg b/linux/assets/file-icons/icons/light/files/supabase.svg new file mode 100644 index 000000000..4d37adde0 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/supabase.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/svelte.svg b/linux/assets/file-icons/icons/light/files/svelte.svg new file mode 100644 index 000000000..2ca6817c6 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/svelte.svg @@ -0,0 +1,7 @@ + + + + + + S + diff --git a/linux/assets/file-icons/icons/light/files/svg.svg b/linux/assets/file-icons/icons/light/files/svg.svg new file mode 100644 index 000000000..360f68910 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/svg.svg @@ -0,0 +1,7 @@ + + + + + + S + diff --git a/linux/assets/file-icons/icons/light/files/swc.svg b/linux/assets/file-icons/icons/light/files/swc.svg new file mode 100644 index 000000000..79eb8ecfb --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/swc.svg @@ -0,0 +1,7 @@ + + + + + + S + diff --git a/linux/assets/file-icons/icons/light/files/swift.svg b/linux/assets/file-icons/icons/light/files/swift.svg new file mode 100644 index 000000000..80d17c02c --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/swift.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/tailwind.svg b/linux/assets/file-icons/icons/light/files/tailwind.svg new file mode 100644 index 000000000..c6b21b1d7 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/tailwind.svg @@ -0,0 +1,7 @@ + + + + + + T + diff --git a/linux/assets/file-icons/icons/light/files/tauri.svg b/linux/assets/file-icons/icons/light/files/tauri.svg new file mode 100644 index 000000000..9a7394cb7 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/tauri.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/terraform.svg b/linux/assets/file-icons/icons/light/files/terraform.svg new file mode 100644 index 000000000..6318401f8 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/terraform.svg @@ -0,0 +1,7 @@ + + + + + + TF + diff --git a/linux/assets/file-icons/icons/light/files/test.svg b/linux/assets/file-icons/icons/light/files/test.svg new file mode 100644 index 000000000..fb610d1ea --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/test.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/text.svg b/linux/assets/file-icons/icons/light/files/text.svg new file mode 100644 index 000000000..7755a7710 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/text.svg @@ -0,0 +1,7 @@ + + + + + + TXT + diff --git a/linux/assets/file-icons/icons/light/files/toml.svg b/linux/assets/file-icons/icons/light/files/toml.svg new file mode 100644 index 000000000..96fb34215 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/toml.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/turborepo.svg b/linux/assets/file-icons/icons/light/files/turborepo.svg new file mode 100644 index 000000000..6ef2337d7 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/turborepo.svg @@ -0,0 +1,7 @@ + + + + + + T + diff --git a/linux/assets/file-icons/icons/light/files/typescript.svg b/linux/assets/file-icons/icons/light/files/typescript.svg new file mode 100644 index 000000000..d52a028e3 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/typescript.svg @@ -0,0 +1,7 @@ + + + + + + TS + diff --git a/linux/assets/file-icons/icons/light/files/vercel-config.svg b/linux/assets/file-icons/icons/light/files/vercel-config.svg new file mode 100644 index 000000000..6c5eb2b1f --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/vercel-config.svg @@ -0,0 +1,7 @@ + + + + + + VC + diff --git a/linux/assets/file-icons/icons/light/files/vercel.svg b/linux/assets/file-icons/icons/light/files/vercel.svg new file mode 100644 index 000000000..64752bab9 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/vercel.svg @@ -0,0 +1,7 @@ + + + + + + V + diff --git a/linux/assets/file-icons/icons/light/files/video.svg b/linux/assets/file-icons/icons/light/files/video.svg new file mode 100644 index 000000000..86cd60bb3 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/video.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/vite.svg b/linux/assets/file-icons/icons/light/files/vite.svg new file mode 100644 index 000000000..3a4e803c6 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/vite.svg @@ -0,0 +1,7 @@ + + + + + + V + diff --git a/linux/assets/file-icons/icons/light/files/vitest.svg b/linux/assets/file-icons/icons/light/files/vitest.svg new file mode 100644 index 000000000..cc4872157 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/vitest.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/vue.svg b/linux/assets/file-icons/icons/light/files/vue.svg new file mode 100644 index 000000000..644da25b9 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/vue.svg @@ -0,0 +1,7 @@ + + + + + + V + diff --git a/linux/assets/file-icons/icons/light/files/warning.svg b/linux/assets/file-icons/icons/light/files/warning.svg new file mode 100644 index 000000000..9e9cb6330 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/warning.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/wasm.svg b/linux/assets/file-icons/icons/light/files/wasm.svg new file mode 100644 index 000000000..3a63d47a9 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/wasm.svg @@ -0,0 +1,7 @@ + + + + + + W + diff --git a/linux/assets/file-icons/icons/light/files/webpack.svg b/linux/assets/file-icons/icons/light/files/webpack.svg new file mode 100644 index 000000000..14cb684b2 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/webpack.svg @@ -0,0 +1,7 @@ + + + + + + W + diff --git a/linux/assets/file-icons/icons/light/files/windows.svg b/linux/assets/file-icons/icons/light/files/windows.svg new file mode 100644 index 000000000..92afb257d --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/windows.svg @@ -0,0 +1,7 @@ + + + + + + W + diff --git a/linux/assets/file-icons/icons/light/files/word.svg b/linux/assets/file-icons/icons/light/files/word.svg new file mode 100644 index 000000000..7f54bb1fc --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/word.svg @@ -0,0 +1,7 @@ + + + + + + DOC + diff --git a/linux/assets/file-icons/icons/light/files/xcode.svg b/linux/assets/file-icons/icons/light/files/xcode.svg new file mode 100644 index 000000000..51a37e666 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/xcode.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/files/xml.svg b/linux/assets/file-icons/icons/light/files/xml.svg new file mode 100644 index 000000000..f1c38169f --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/xml.svg @@ -0,0 +1,7 @@ + + + + + + <> + diff --git a/linux/assets/file-icons/icons/light/files/yaml.svg b/linux/assets/file-icons/icons/light/files/yaml.svg new file mode 100644 index 000000000..8465e7e70 --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/yaml.svg @@ -0,0 +1,7 @@ + + + + + + Y + diff --git a/linux/assets/file-icons/icons/light/files/yarn.svg b/linux/assets/file-icons/icons/light/files/yarn.svg new file mode 100644 index 000000000..05219d53a --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/yarn.svg @@ -0,0 +1,7 @@ + + + + + + Y + diff --git a/linux/assets/file-icons/icons/light/files/zig.svg b/linux/assets/file-icons/icons/light/files/zig.svg new file mode 100644 index 000000000..c529bd03a --- /dev/null +++ b/linux/assets/file-icons/icons/light/files/zig.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-ai-open.svg b/linux/assets/file-icons/icons/light/folders/folder-ai-open.svg new file mode 100644 index 000000000..5e951884f --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-ai-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-ai.svg b/linux/assets/file-icons/icons/light/folders/folder-ai.svg new file mode 100644 index 000000000..44f8c72aa --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-ai.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-assets-open.svg b/linux/assets/file-icons/icons/light/folders/folder-assets-open.svg new file mode 100644 index 000000000..88538645e --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-assets-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-assets.svg b/linux/assets/file-icons/icons/light/folders/folder-assets.svg new file mode 100644 index 000000000..94d68d0b7 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-assets.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-build-open.svg b/linux/assets/file-icons/icons/light/folders/folder-build-open.svg new file mode 100644 index 000000000..690053176 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-build-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-build.svg b/linux/assets/file-icons/icons/light/folders/folder-build.svg new file mode 100644 index 000000000..8e17e72f2 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-build.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-cloud-open.svg b/linux/assets/file-icons/icons/light/folders/folder-cloud-open.svg new file mode 100644 index 000000000..52f4a926a --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-cloud-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-cloud.svg b/linux/assets/file-icons/icons/light/folders/folder-cloud.svg new file mode 100644 index 000000000..c0bce4b87 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-cloud.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-components-open.svg b/linux/assets/file-icons/icons/light/folders/folder-components-open.svg new file mode 100644 index 000000000..7ddb8b0d3 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-components-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-components.svg b/linux/assets/file-icons/icons/light/folders/folder-components.svg new file mode 100644 index 000000000..95b2529da --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-components.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-config-open.svg b/linux/assets/file-icons/icons/light/folders/folder-config-open.svg new file mode 100644 index 000000000..b2bd27620 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-config-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-config.svg b/linux/assets/file-icons/icons/light/folders/folder-config.svg new file mode 100644 index 000000000..2a9817835 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-config.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-database-open.svg b/linux/assets/file-icons/icons/light/folders/folder-database-open.svg new file mode 100644 index 000000000..6755b5694 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-database-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-database.svg b/linux/assets/file-icons/icons/light/folders/folder-database.svg new file mode 100644 index 000000000..8ac9462ab --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-database.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-docs-open.svg b/linux/assets/file-icons/icons/light/folders/folder-docs-open.svg new file mode 100644 index 000000000..f244dc895 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-docs-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-docs.svg b/linux/assets/file-icons/icons/light/folders/folder-docs.svg new file mode 100644 index 000000000..88f62bd59 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-docs.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-extensions-open.svg b/linux/assets/file-icons/icons/light/folders/folder-extensions-open.svg new file mode 100644 index 000000000..3119706f1 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-extensions-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-extensions.svg b/linux/assets/file-icons/icons/light/folders/folder-extensions.svg new file mode 100644 index 000000000..2e24257d9 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-extensions.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-git-open.svg b/linux/assets/file-icons/icons/light/folders/folder-git-open.svg new file mode 100644 index 000000000..946e9cdeb --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-git-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-git.svg b/linux/assets/file-icons/icons/light/folders/folder-git.svg new file mode 100644 index 000000000..f0b06065a --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-git.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-locales-open.svg b/linux/assets/file-icons/icons/light/folders/folder-locales-open.svg new file mode 100644 index 000000000..73c3d1386 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-locales-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-locales.svg b/linux/assets/file-icons/icons/light/folders/folder-locales.svg new file mode 100644 index 000000000..6fc668015 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-locales.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-mobile-open.svg b/linux/assets/file-icons/icons/light/folders/folder-mobile-open.svg new file mode 100644 index 000000000..243086c22 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-mobile-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-mobile.svg b/linux/assets/file-icons/icons/light/folders/folder-mobile.svg new file mode 100644 index 000000000..7fa4504b5 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-mobile.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-open.svg b/linux/assets/file-icons/icons/light/folders/folder-open.svg new file mode 100644 index 000000000..2dc12ef87 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-open.svg @@ -0,0 +1,4 @@ + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-packages-open.svg b/linux/assets/file-icons/icons/light/folders/folder-packages-open.svg new file mode 100644 index 000000000..1c7e10af4 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-packages-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-packages.svg b/linux/assets/file-icons/icons/light/folders/folder-packages.svg new file mode 100644 index 000000000..0663dd214 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-packages.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-routes-open.svg b/linux/assets/file-icons/icons/light/folders/folder-routes-open.svg new file mode 100644 index 000000000..2e6036a73 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-routes-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-routes.svg b/linux/assets/file-icons/icons/light/folders/folder-routes.svg new file mode 100644 index 000000000..c88e28f75 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-routes.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-rust-open.svg b/linux/assets/file-icons/icons/light/folders/folder-rust-open.svg new file mode 100644 index 000000000..297e2377c --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-rust-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-rust.svg b/linux/assets/file-icons/icons/light/folders/folder-rust.svg new file mode 100644 index 000000000..35e5e2327 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-rust.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-scripts-open.svg b/linux/assets/file-icons/icons/light/folders/folder-scripts-open.svg new file mode 100644 index 000000000..519b6138c --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-scripts-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-scripts.svg b/linux/assets/file-icons/icons/light/folders/folder-scripts.svg new file mode 100644 index 000000000..78fa6406f --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-scripts.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-security-open.svg b/linux/assets/file-icons/icons/light/folders/folder-security-open.svg new file mode 100644 index 000000000..4888dcd72 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-security-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-security.svg b/linux/assets/file-icons/icons/light/folders/folder-security.svg new file mode 100644 index 000000000..1aa7f1769 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-security.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-source-open.svg b/linux/assets/file-icons/icons/light/folders/folder-source-open.svg new file mode 100644 index 000000000..1da3d2d4b --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-source-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-source.svg b/linux/assets/file-icons/icons/light/folders/folder-source.svg new file mode 100644 index 000000000..dd71d5eb7 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-source.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-styles-open.svg b/linux/assets/file-icons/icons/light/folders/folder-styles-open.svg new file mode 100644 index 000000000..b3bf8b742 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-styles-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-styles.svg b/linux/assets/file-icons/icons/light/folders/folder-styles.svg new file mode 100644 index 000000000..060ce9d00 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-styles.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-test-open.svg b/linux/assets/file-icons/icons/light/folders/folder-test-open.svg new file mode 100644 index 000000000..7adcf0d59 --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-test-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder-test.svg b/linux/assets/file-icons/icons/light/folders/folder-test.svg new file mode 100644 index 000000000..2fd97785a --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder-test.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/linux/assets/file-icons/icons/light/folders/folder.svg b/linux/assets/file-icons/icons/light/folders/folder.svg new file mode 100644 index 000000000..a127080bf --- /dev/null +++ b/linux/assets/file-icons/icons/light/folders/folder.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..53f5e9925 --- /dev/null +++ b/linux/assets/file-icons/map.json @@ -0,0 +1 @@ +{"defaultFile": "file", "defaultFolder": "folder", "defaultFolderOpen": "folder-open", "iconDefinitions": {"file": "./icons/files/file.svg", "text": "./icons/files/text.svg", "document": "./icons/files/document.svg", "markdown": "./icons/files/markdown.svg", "html": "./icons/files/html.svg", "css": "./icons/files/css.svg", "sass": "./icons/files/sass.svg", "javascript": "./icons/files/javascript.svg", "typescript": "./icons/files/typescript.svg", "react": "./icons/files/react.svg", "vue": "./icons/files/vue.svg", "svelte": "./icons/files/svelte.svg", "astro": "./icons/files/astro.svg", "json": "./icons/files/json.svg", "yaml": "./icons/files/yaml.svg", "toml": "./icons/files/toml.svg", "xml": "./icons/files/xml.svg", "rust": "./icons/files/rust.svg", "python": "./icons/files/python.svg", "go": "./icons/files/go.svg", "java": "./icons/files/java.svg", "c": "./icons/files/c.svg", "cpp": "./icons/files/cpp.svg", "csharp": "./icons/files/csharp.svg", "swift": "./icons/files/swift.svg", "zig": "./icons/files/zig.svg", "ruby": "./icons/files/ruby.svg", "php": "./icons/files/php.svg", "shell": "./icons/files/shell.svg", "sql": "./icons/files/sql.svg", "database": "./icons/files/database.svg", "prisma": "./icons/files/prisma.svg", "graphql": "./icons/files/graphql.svg", "docker": "./icons/files/docker.svg", "git": "./icons/files/git.svg", "github": "./icons/files/github.svg", "package": "./icons/files/package.svg", "node": "./icons/files/node.svg", "bun": "./icons/files/bun.svg", "deno": "./icons/files/deno.svg", "lock": "./icons/files/lock.svg", "config": "./icons/files/config.svg", "env": "./icons/files/env.svg", "test": "./icons/files/test.svg", "vite": "./icons/files/vite.svg", "tailwind": "./icons/files/tailwind.svg", "image": "./icons/files/image.svg", "svg": "./icons/files/svg.svg", "audio": "./icons/files/audio.svg", "video": "./icons/files/video.svg", "font": "./icons/files/font.svg", "pdf": "./icons/files/pdf.svg", "notebook": "./icons/files/notebook.svg", "next": "./icons/files/next.svg", "nuxt": "./icons/files/nuxt.svg", "angular": "./icons/files/angular.svg", "solid": "./icons/files/solid.svg", "remix": "./icons/files/remix.svg", "qwik": "./icons/files/qwik.svg", "lit": "./icons/files/lit.svg", "storybook": "./icons/files/storybook.svg", "jest": "./icons/files/jest.svg", "vitest": "./icons/files/vitest.svg", "playwright": "./icons/files/playwright.svg", "cypress": "./icons/files/cypress.svg", "eslint": "./icons/files/eslint.svg", "prettier": "./icons/files/prettier.svg", "biome": "./icons/files/biome.svg", "babel": "./icons/files/babel.svg", "swc": "./icons/files/swc.svg", "webpack": "./icons/files/webpack.svg", "rollup": "./icons/files/rollup.svg", "rspack": "./icons/files/rspack.svg", "turborepo": "./icons/files/turborepo.svg", "nx": "./icons/files/nx.svg", "npm": "./icons/files/npm.svg", "pnpm": "./icons/files/pnpm.svg", "yarn": "./icons/files/yarn.svg", "maven": "./icons/files/maven.svg", "gradle": "./icons/files/gradle.svg", "kotlin": "./icons/files/kotlin.svg", "dart": "./icons/files/dart.svg", "lua": "./icons/files/lua.svg", "elixir": "./icons/files/elixir.svg", "erlang": "./icons/files/erlang.svg", "haskell": "./icons/files/haskell.svg", "scala": "./icons/files/scala.svg", "clojure": "./icons/files/clojure.svg", "nim": "./icons/files/nim.svg", "nix": "./icons/files/nix.svg", "terraform": "./icons/files/terraform.svg", "kubernetes": "./icons/files/kubernetes.svg", "helm": "./icons/files/helm.svg", "ansible": "./icons/files/ansible.svg", "cloudflare": "./icons/files/cloudflare.svg", "netlify": "./icons/files/netlify.svg", "vercel": "./icons/files/vercel.svg", "firebase": "./icons/files/firebase.svg", "supabase": "./icons/files/supabase.svg", "mongo": "./icons/files/mongo.svg", "redis": "./icons/files/redis.svg", "postgres": "./icons/files/postgres.svg", "drizzle": "./icons/files/drizzle.svg", "figma": "./icons/files/figma.svg", "sketch": "./icons/files/sketch.svg", "adobe": "./icons/files/adobe.svg", "csv": "./icons/files/csv.svg", "spreadsheet": "./icons/files/spreadsheet.svg", "word": "./icons/files/word.svg", "powerpoint": "./icons/files/powerpoint.svg", "archive": "./icons/files/archive.svg", "certificate": "./icons/files/certificate.svg", "key": "./icons/files/key.svg", "log": "./icons/files/log.svg", "diff": "./icons/files/diff.svg", "patch": "./icons/files/patch.svg", "license": "./icons/files/license.svg", "makefile": "./icons/files/makefile.svg", "cmake": "./icons/files/cmake.svg", "proto": "./icons/files/proto.svg", "wasm": "./icons/files/wasm.svg", "rescript": "./icons/files/rescript.svg", "ocaml": "./icons/files/ocaml.svg", "solidity": "./icons/files/solidity.svg", "r": "./icons/files/r.svg", "julia": "./icons/files/julia.svg", "perl": "./icons/files/perl.svg", "lithe": "./icons/files/lithe.svg", "codex": "./icons/files/codex.svg", "claude": "./icons/files/claude.svg", "cursor": "./icons/files/cursor.svg", "tauri": "./icons/files/tauri.svg", "electron": "./icons/files/electron.svg", "xcode": "./icons/files/xcode.svg", "android": "./icons/files/android.svg", "apple": "./icons/files/apple.svg", "windows": "./icons/files/windows.svg", "linux": "./icons/files/linux.svg", "changelog": "./icons/files/changelog.svg", "authors": "./icons/files/authors.svg", "security": "./icons/files/security.svg", "warning": "./icons/files/warning.svg", "agents": "./icons/files/agents.svg", "copilot": "./icons/files/copilot.svg", "gemini": "./icons/files/gemini.svg", "cline": "./icons/files/cline.svg", "mcp": "./icons/files/mcp.svg", "editorconfig": "./icons/files/editorconfig.svg", "stylelint": "./icons/files/stylelint.svg", "markdownlint": "./icons/files/markdownlint.svg", "cspell": "./icons/files/cspell.svg", "commitlint": "./icons/files/commitlint.svg", "lintstaged": "./icons/files/lintstaged.svg", "renovate": "./icons/files/renovate.svg", "dependabot": "./icons/files/dependabot.svg", "docker-compose": "./icons/files/docker-compose.svg", "devcontainer": "./icons/files/devcontainer.svg", "github-actions": "./icons/files/github-actions.svg", "gitlab": "./icons/files/gitlab.svg", "bitbucket": "./icons/files/bitbucket.svg", "jenkins": "./icons/files/jenkins.svg", "vercel-config": "./icons/files/vercel-config.svg", "nginx": "./icons/files/nginx.svg", "http": "./icons/files/http.svg", "hurl": "./icons/files/hurl.svg", "graphql-schema": "./icons/files/graphql-schema.svg", "jsconfig": "./icons/files/jsconfig.svg", "index-js": "./icons/files/index-js.svg", "index-ts": "./icons/files/index-ts.svg", "layout": "./icons/files/layout.svg", "page": "./icons/files/page.svg", "route": "./icons/files/route.svg", "loading": "./icons/files/loading.svg", "not-found": "./icons/files/not-found.svg", "error": "./icons/files/error.svg", "docusaurus": "./icons/files/docusaurus.svg", "gatsby": "./icons/files/gatsby.svg", "laravel": "./icons/files/laravel.svg", "django": "./icons/files/django.svg", "flask": "./icons/files/flask.svg", "fastapi": "./icons/files/fastapi.svg", "arduino": "./icons/files/arduino.svg", "blender": "./icons/files/blender.svg", "drawio": "./icons/files/drawio.svg", "excalidraw": "./icons/files/excalidraw.svg", "mermaid": "./icons/files/mermaid.svg", "folder": "./icons/folders/folder.svg", "folder-open": "./icons/folders/folder-open.svg", "folder-source": "./icons/folders/folder-source.svg", "folder-source-open": "./icons/folders/folder-source-open.svg", "folder-components": "./icons/folders/folder-components.svg", "folder-components-open": "./icons/folders/folder-components-open.svg", "folder-test": "./icons/folders/folder-test.svg", "folder-test-open": "./icons/folders/folder-test-open.svg", "folder-config": "./icons/folders/folder-config.svg", "folder-config-open": "./icons/folders/folder-config-open.svg", "folder-assets": "./icons/folders/folder-assets.svg", "folder-assets-open": "./icons/folders/folder-assets-open.svg", "folder-docs": "./icons/folders/folder-docs.svg", "folder-docs-open": "./icons/folders/folder-docs-open.svg", "folder-scripts": "./icons/folders/folder-scripts.svg", "folder-scripts-open": "./icons/folders/folder-scripts-open.svg", "folder-rust": "./icons/folders/folder-rust.svg", "folder-rust-open": "./icons/folders/folder-rust-open.svg", "folder-packages": "./icons/folders/folder-packages.svg", "folder-packages-open": "./icons/folders/folder-packages-open.svg", "folder-git": "./icons/folders/folder-git.svg", "folder-git-open": "./icons/folders/folder-git-open.svg", "folder-build": "./icons/folders/folder-build.svg", "folder-build-open": "./icons/folders/folder-build-open.svg", "folder-database": "./icons/folders/folder-database.svg", "folder-database-open": "./icons/folders/folder-database-open.svg", "folder-routes": "./icons/folders/folder-routes.svg", "folder-routes-open": "./icons/folders/folder-routes-open.svg", "folder-styles": "./icons/folders/folder-styles.svg", "folder-styles-open": "./icons/folders/folder-styles-open.svg", "folder-locales": "./icons/folders/folder-locales.svg", "folder-locales-open": "./icons/folders/folder-locales-open.svg", "folder-cloud": "./icons/folders/folder-cloud.svg", "folder-cloud-open": "./icons/folders/folder-cloud-open.svg", "folder-mobile": "./icons/folders/folder-mobile.svg", "folder-mobile-open": "./icons/folders/folder-mobile-open.svg", "folder-security": "./icons/folders/folder-security.svg", "folder-security-open": "./icons/folders/folder-security-open.svg", "folder-ai": "./icons/folders/folder-ai.svg", "folder-ai-open": "./icons/folders/folder-ai-open.svg", "folder-extensions": "./icons/folders/folder-extensions.svg", "folder-extensions-open": "./icons/folders/folder-extensions-open.svg"}, "lightIconDefinitions": {"file": "./icons/light/files/file.svg", "text": "./icons/light/files/text.svg", "document": "./icons/light/files/document.svg", "markdown": "./icons/light/files/markdown.svg", "html": "./icons/light/files/html.svg", "css": "./icons/light/files/css.svg", "sass": "./icons/light/files/sass.svg", "javascript": "./icons/light/files/javascript.svg", "typescript": "./icons/light/files/typescript.svg", "react": "./icons/light/files/react.svg", "vue": "./icons/light/files/vue.svg", "svelte": "./icons/light/files/svelte.svg", "astro": "./icons/light/files/astro.svg", "json": "./icons/light/files/json.svg", "yaml": "./icons/light/files/yaml.svg", "toml": "./icons/light/files/toml.svg", "xml": "./icons/light/files/xml.svg", "rust": "./icons/light/files/rust.svg", "python": "./icons/light/files/python.svg", "go": "./icons/light/files/go.svg", "java": "./icons/light/files/java.svg", "c": "./icons/light/files/c.svg", "cpp": "./icons/light/files/cpp.svg", "csharp": "./icons/light/files/csharp.svg", "swift": "./icons/light/files/swift.svg", "zig": "./icons/light/files/zig.svg", "ruby": "./icons/light/files/ruby.svg", "php": "./icons/light/files/php.svg", "shell": "./icons/light/files/shell.svg", "sql": "./icons/light/files/sql.svg", "database": "./icons/light/files/database.svg", "prisma": "./icons/light/files/prisma.svg", "graphql": "./icons/light/files/graphql.svg", "docker": "./icons/light/files/docker.svg", "git": "./icons/light/files/git.svg", "github": "./icons/light/files/github.svg", "package": "./icons/light/files/package.svg", "node": "./icons/light/files/node.svg", "bun": "./icons/light/files/bun.svg", "deno": "./icons/light/files/deno.svg", "lock": "./icons/light/files/lock.svg", "config": "./icons/light/files/config.svg", "env": "./icons/light/files/env.svg", "test": "./icons/light/files/test.svg", "vite": "./icons/light/files/vite.svg", "tailwind": "./icons/light/files/tailwind.svg", "image": "./icons/light/files/image.svg", "svg": "./icons/light/files/svg.svg", "audio": "./icons/light/files/audio.svg", "video": "./icons/light/files/video.svg", "font": "./icons/light/files/font.svg", "pdf": "./icons/light/files/pdf.svg", "notebook": "./icons/light/files/notebook.svg", "next": "./icons/light/files/next.svg", "nuxt": "./icons/light/files/nuxt.svg", "angular": "./icons/light/files/angular.svg", "solid": "./icons/light/files/solid.svg", "remix": "./icons/light/files/remix.svg", "qwik": "./icons/light/files/qwik.svg", "lit": "./icons/light/files/lit.svg", "storybook": "./icons/light/files/storybook.svg", "jest": "./icons/light/files/jest.svg", "vitest": "./icons/light/files/vitest.svg", "playwright": "./icons/light/files/playwright.svg", "cypress": "./icons/light/files/cypress.svg", "eslint": "./icons/light/files/eslint.svg", "prettier": "./icons/light/files/prettier.svg", "biome": "./icons/light/files/biome.svg", "babel": "./icons/light/files/babel.svg", "swc": "./icons/light/files/swc.svg", "webpack": "./icons/light/files/webpack.svg", "rollup": "./icons/light/files/rollup.svg", "rspack": "./icons/light/files/rspack.svg", "turborepo": "./icons/light/files/turborepo.svg", "nx": "./icons/light/files/nx.svg", "npm": "./icons/light/files/npm.svg", "pnpm": "./icons/light/files/pnpm.svg", "yarn": "./icons/light/files/yarn.svg", "maven": "./icons/light/files/maven.svg", "gradle": "./icons/light/files/gradle.svg", "kotlin": "./icons/light/files/kotlin.svg", "dart": "./icons/light/files/dart.svg", "lua": "./icons/light/files/lua.svg", "elixir": "./icons/light/files/elixir.svg", "erlang": "./icons/light/files/erlang.svg", "haskell": "./icons/light/files/haskell.svg", "scala": "./icons/light/files/scala.svg", "clojure": "./icons/light/files/clojure.svg", "nim": "./icons/light/files/nim.svg", "nix": "./icons/light/files/nix.svg", "terraform": "./icons/light/files/terraform.svg", "kubernetes": "./icons/light/files/kubernetes.svg", "helm": "./icons/light/files/helm.svg", "ansible": "./icons/light/files/ansible.svg", "cloudflare": "./icons/light/files/cloudflare.svg", "netlify": "./icons/light/files/netlify.svg", "vercel": "./icons/light/files/vercel.svg", "firebase": "./icons/light/files/firebase.svg", "supabase": "./icons/light/files/supabase.svg", "mongo": "./icons/light/files/mongo.svg", "redis": "./icons/light/files/redis.svg", "postgres": "./icons/light/files/postgres.svg", "drizzle": "./icons/light/files/drizzle.svg", "figma": "./icons/light/files/figma.svg", "sketch": "./icons/light/files/sketch.svg", "adobe": "./icons/light/files/adobe.svg", "csv": "./icons/light/files/csv.svg", "spreadsheet": "./icons/light/files/spreadsheet.svg", "word": "./icons/light/files/word.svg", "powerpoint": "./icons/light/files/powerpoint.svg", "archive": "./icons/light/files/archive.svg", "certificate": "./icons/light/files/certificate.svg", "key": "./icons/light/files/key.svg", "log": "./icons/light/files/log.svg", "diff": "./icons/light/files/diff.svg", "patch": "./icons/light/files/patch.svg", "license": "./icons/light/files/license.svg", "makefile": "./icons/light/files/makefile.svg", "cmake": "./icons/light/files/cmake.svg", "proto": "./icons/light/files/proto.svg", "wasm": "./icons/light/files/wasm.svg", "rescript": "./icons/light/files/rescript.svg", "ocaml": "./icons/light/files/ocaml.svg", "solidity": "./icons/light/files/solidity.svg", "r": "./icons/light/files/r.svg", "julia": "./icons/light/files/julia.svg", "perl": "./icons/light/files/perl.svg", "lithe": "./icons/light/files/lithe.svg", "codex": "./icons/light/files/codex.svg", "claude": "./icons/light/files/claude.svg", "cursor": "./icons/light/files/cursor.svg", "tauri": "./icons/light/files/tauri.svg", "electron": "./icons/light/files/electron.svg", "xcode": "./icons/light/files/xcode.svg", "android": "./icons/light/files/android.svg", "apple": "./icons/light/files/apple.svg", "windows": "./icons/light/files/windows.svg", "linux": "./icons/light/files/linux.svg", "changelog": "./icons/light/files/changelog.svg", "authors": "./icons/light/files/authors.svg", "security": "./icons/light/files/security.svg", "warning": "./icons/light/files/warning.svg", "agents": "./icons/light/files/agents.svg", "copilot": "./icons/light/files/copilot.svg", "gemini": "./icons/light/files/gemini.svg", "cline": "./icons/light/files/cline.svg", "mcp": "./icons/light/files/mcp.svg", "editorconfig": "./icons/light/files/editorconfig.svg", "stylelint": "./icons/light/files/stylelint.svg", "markdownlint": "./icons/light/files/markdownlint.svg", "cspell": "./icons/light/files/cspell.svg", "commitlint": "./icons/light/files/commitlint.svg", "lintstaged": "./icons/light/files/lintstaged.svg", "renovate": "./icons/light/files/renovate.svg", "dependabot": "./icons/light/files/dependabot.svg", "docker-compose": "./icons/light/files/docker-compose.svg", "devcontainer": "./icons/light/files/devcontainer.svg", "github-actions": "./icons/light/files/github-actions.svg", "gitlab": "./icons/light/files/gitlab.svg", "bitbucket": "./icons/light/files/bitbucket.svg", "jenkins": "./icons/light/files/jenkins.svg", "vercel-config": "./icons/light/files/vercel-config.svg", "nginx": "./icons/light/files/nginx.svg", "http": "./icons/light/files/http.svg", "hurl": "./icons/light/files/hurl.svg", "graphql-schema": "./icons/light/files/graphql-schema.svg", "jsconfig": "./icons/light/files/jsconfig.svg", "index-js": "./icons/light/files/index-js.svg", "index-ts": "./icons/light/files/index-ts.svg", "layout": "./icons/light/files/layout.svg", "page": "./icons/light/files/page.svg", "route": "./icons/light/files/route.svg", "loading": "./icons/light/files/loading.svg", "not-found": "./icons/light/files/not-found.svg", "error": "./icons/light/files/error.svg", "docusaurus": "./icons/light/files/docusaurus.svg", "gatsby": "./icons/light/files/gatsby.svg", "laravel": "./icons/light/files/laravel.svg", "django": "./icons/light/files/django.svg", "flask": "./icons/light/files/flask.svg", "fastapi": "./icons/light/files/fastapi.svg", "arduino": "./icons/light/files/arduino.svg", "blender": "./icons/light/files/blender.svg", "drawio": "./icons/light/files/drawio.svg", "excalidraw": "./icons/light/files/excalidraw.svg", "mermaid": "./icons/light/files/mermaid.svg", "folder": "./icons/light/folders/folder.svg", "folder-open": "./icons/light/folders/folder-open.svg", "folder-source": "./icons/light/folders/folder-source.svg", "folder-source-open": "./icons/light/folders/folder-source-open.svg", "folder-components": "./icons/light/folders/folder-components.svg", "folder-components-open": "./icons/light/folders/folder-components-open.svg", "folder-test": "./icons/light/folders/folder-test.svg", "folder-test-open": "./icons/light/folders/folder-test-open.svg", "folder-config": "./icons/light/folders/folder-config.svg", "folder-config-open": "./icons/light/folders/folder-config-open.svg", "folder-assets": "./icons/light/folders/folder-assets.svg", "folder-assets-open": "./icons/light/folders/folder-assets-open.svg", "folder-docs": "./icons/light/folders/folder-docs.svg", "folder-docs-open": "./icons/light/folders/folder-docs-open.svg", "folder-scripts": "./icons/light/folders/folder-scripts.svg", "folder-scripts-open": "./icons/light/folders/folder-scripts-open.svg", "folder-rust": "./icons/light/folders/folder-rust.svg", "folder-rust-open": "./icons/light/folders/folder-rust-open.svg", "folder-packages": "./icons/light/folders/folder-packages.svg", "folder-packages-open": "./icons/light/folders/folder-packages-open.svg", "folder-git": "./icons/light/folders/folder-git.svg", "folder-git-open": "./icons/light/folders/folder-git-open.svg", "folder-build": "./icons/light/folders/folder-build.svg", "folder-build-open": "./icons/light/folders/folder-build-open.svg", "folder-database": "./icons/light/folders/folder-database.svg", "folder-database-open": "./icons/light/folders/folder-database-open.svg", "folder-routes": "./icons/light/folders/folder-routes.svg", "folder-routes-open": "./icons/light/folders/folder-routes-open.svg", "folder-styles": "./icons/light/folders/folder-styles.svg", "folder-styles-open": "./icons/light/folders/folder-styles-open.svg", "folder-locales": "./icons/light/folders/folder-locales.svg", "folder-locales-open": "./icons/light/folders/folder-locales-open.svg", "folder-cloud": "./icons/light/folders/folder-cloud.svg", "folder-cloud-open": "./icons/light/folders/folder-cloud-open.svg", "folder-mobile": "./icons/light/folders/folder-mobile.svg", "folder-mobile-open": "./icons/light/folders/folder-mobile-open.svg", "folder-security": "./icons/light/folders/folder-security.svg", "folder-security-open": "./icons/light/folders/folder-security-open.svg", "folder-ai": "./icons/light/folders/folder-ai.svg", "folder-ai-open": "./icons/light/folders/folder-ai-open.svg", "folder-extensions": "./icons/light/folders/folder-extensions.svg", "folder-extensions-open": "./icons/light/folders/folder-extensions-open.svg"}, "fileExtensions": {".txt": "text", ".md": "markdown", ".mdx": "markdown", ".html": "html", ".htm": "html", ".css": "css", ".scss": "sass", ".sass": "sass", ".js": "javascript", ".mjs": "javascript", ".cjs": "javascript", ".jsx": "react", ".ts": "typescript", ".mts": "typescript", ".cts": "typescript", ".tsx": "react", ".vue": "vue", ".svelte": "svelte", ".astro": "astro", ".json": "json", ".jsonc": "json", ".yaml": "yaml", ".yml": "yaml", ".toml": "toml", ".xml": "xml", ".svg": "svg", ".rs": "rust", ".ron": "rust", ".py": "python", ".go": "go", ".java": "java", ".c": "c", ".h": "c", ".cpp": "cpp", ".cxx": "cpp", ".cc": "cpp", ".hpp": "cpp", ".cs": "csharp", ".swift": "swift", ".zig": "zig", ".rb": "ruby", ".php": "php", ".sh": "shell", ".bash": "shell", ".zsh": "shell", ".fish": "shell", ".sql": "sql", ".sqlite": "database", ".sqlite3": "database", ".db": "database", ".prisma": "prisma", ".graphql": "graphql", ".gql": "graphql", ".dockerfile": "docker", ".lock": "lock", ".env": "env", ".test.js": "test", ".test.ts": "test", ".test.tsx": "test", ".spec.js": "test", ".spec.ts": "test", ".spec.tsx": "test", ".png": "image", ".jpg": "image", ".jpeg": "image", ".webp": "image", ".gif": "image", ".ico": "image", ".mp3": "audio", ".wav": "audio", ".flac": "audio", ".mp4": "video", ".mov": "video", ".webm": "video", ".ttf": "font", ".otf": "font", ".woff": "font", ".woff2": "font", ".pdf": "pdf", ".ipynb": "notebook", ".vue.ts": "vue", ".svelte.ts": "svelte", ".stories.js": "storybook", ".stories.jsx": "storybook", ".stories.ts": "storybook", ".stories.tsx": "storybook", ".story.js": "storybook", ".story.jsx": "storybook", ".story.ts": "storybook", ".story.tsx": "storybook", ".cy.js": "cypress", ".cy.ts": "cypress", ".cy.tsx": "cypress", ".playwright.js": "playwright", ".playwright.ts": "playwright", ".test.mjs": "test", ".spec.mjs": "test", ".kt": "kotlin", ".kts": "kotlin", ".dart": "dart", ".lua": "lua", ".ex": "elixir", ".exs": "elixir", ".erl": "erlang", ".hrl": "erlang", ".hs": "haskell", ".scala": "scala", ".sc": "scala", ".clj": "clojure", ".cljs": "clojure", ".nim": "nim", ".nix": "nix", ".tf": "terraform", ".tfvars": "terraform", ".hcl": "terraform", ".k8s.yaml": "kubernetes", ".helm.yaml": "helm", ".yarnrc": "yarn", ".npmrc": "npm", ".csv": "csv", ".tsv": "csv", ".xlsx": "spreadsheet", ".xls": "spreadsheet", ".doc": "word", ".docx": "word", ".ppt": "powerpoint", ".pptx": "powerpoint", ".zip": "archive", ".tar": "archive", ".gz": "archive", ".tgz": "archive", ".rar": "archive", ".7z": "archive", ".pem": "certificate", ".crt": "certificate", ".cer": "certificate", ".key": "key", ".log": "log", ".diff": "diff", ".patch": "patch", ".proto": "proto", ".wasm": "wasm", ".res": "rescript", ".resi": "rescript", ".ml": "ocaml", ".mli": "ocaml", ".sol": "solidity", ".r": "r", ".rmd": "r", ".jl": "julia", ".pl": "perl", ".app": "apple", ".ipa": "apple", ".apk": "android", ".aab": "android", ".exe": "windows", ".msi": "windows", ".dll": "windows", ".so": "linux", ".http": "http", ".rest": "http", ".hurl": "hurl", ".drawio": "drawio", ".excalidraw": "excalidraw", ".mmd": "mermaid", ".mermaid": "mermaid", ".blend": "blender", ".ino": "arduino"}, "filenames": {"package.json": "node", "package-lock.json": "node", "bun.lock": "bun", "bun.lockb": "bun", "bunfig.toml": "bun", "deno.json": "deno", "deno.jsonc": "deno", "tsconfig.json": "typescript", "vite.config.js": "vite", "vite.config.ts": "vite", "tailwind.config.js": "tailwind", "tailwind.config.ts": "tailwind", "dockerfile": "docker", ".dockerignore": "docker", ".gitignore": "git", ".gitattributes": "git", ".env": "env", ".env.example": "env", "cargo.toml": "rust", "cargo.lock": "rust", "readme.md": "markdown", "license": "license", "license.md": "license", "next.config.js": "next", "next.config.mjs": "next", "next.config.ts": "next", "nuxt.config.js": "nuxt", "nuxt.config.ts": "nuxt", "angular.json": "angular", "remix.config.js": "remix", "remix.config.ts": "remix", "qwik.config.js": "qwik", "qwik.config.ts": "qwik", "lit.config.js": "lit", "storybook.config.js": "storybook", "jest.config.js": "jest", "jest.config.ts": "jest", "vitest.config.js": "vitest", "vitest.config.ts": "vitest", "playwright.config.js": "playwright", "playwright.config.ts": "playwright", "cypress.config.js": "cypress", "cypress.config.ts": "cypress", ".eslintrc": "eslint", ".eslintrc.json": "eslint", "eslint.config.js": "eslint", "eslint.config.mjs": "eslint", "eslint.config.ts": "eslint", ".prettierrc": "prettier", ".prettierrc.json": "prettier", "prettier.config.js": "prettier", "biome.json": "biome", "biome.jsonc": "biome", "babel.config.js": "babel", ".babelrc": "babel", ".swcrc": "swc", "webpack.config.js": "webpack", "webpack.config.ts": "webpack", "rollup.config.js": "rollup", "rollup.config.ts": "rollup", "rspack.config.js": "rspack", "rspack.config.ts": "rspack", "turbo.json": "turborepo", "nx.json": "nx", "pnpm-lock.yaml": "pnpm", "pnpm-workspace.yaml": "pnpm", "yarn.lock": "yarn", "pom.xml": "maven", "build.gradle": "gradle", "build.gradle.kts": "gradle", "gradle.properties": "gradle", "gradlew": "gradle", "flake.nix": "nix", "terraform.tfvars": "terraform", "chart.yaml": "helm", "ansible.cfg": "ansible", "wrangler.toml": "cloudflare", "netlify.toml": "netlify", "vercel.json": "vercel", "firebase.json": "firebase", "supabase.toml": "supabase", "drizzle.config.ts": "drizzle", ".fig": "figma", "makefile": "makefile", "cmakelists.txt": "cmake", "copying": "license", "lithe.json": "lithe", ".codex": "codex", "agents.md": "codex", "claude.md": "claude", ".cursorrules": "cursor", "tauri.conf.json": "tauri", "electron-builder.json": "electron", "xcodeproj": "xcode", "androidmanifest.xml": "android", "changelog.md": "changelog", "authors": "authors", "authors.md": "authors", "security.md": "security", "codeowners": "security", ".agents": "agents", "agents.json": "agents", "agents.toml": "agents", "copilot-instructions.md": "copilot", ".mcp.json": "mcp", "mcp.json": "mcp", ".editorconfig": "editorconfig", ".stylelintrc": "stylelint", ".stylelintrc.json": "stylelint", "stylelint.config.js": "stylelint", "stylelint.config.mjs": "stylelint", ".markdownlint.json": "markdownlint", ".markdownlint.yaml": "markdownlint", ".markdownlintignore": "markdownlint", "cspell.json": "cspell", ".cspell.json": "cspell", "commitlint.config.js": "commitlint", "commitlint.config.ts": "commitlint", ".lintstagedrc": "lintstaged", "lint-staged.config.js": "lintstaged", "renovate.json": "renovate", "dependabot.yml": "dependabot", "docker-compose.yml": "docker-compose", "docker-compose.yaml": "docker-compose", ".devcontainer.json": "devcontainer", "devcontainer.json": "devcontainer", "action.yml": "github-actions", "action.yaml": "github-actions", ".gitlab-ci.yml": "gitlab", "bitbucket-pipelines.yml": "bitbucket", "jenkinsfile": "jenkins", "nginx.conf": "nginx", "index.js": "index-js", "index.ts": "index-ts", "index.tsx": "index-ts", "layout.js": "layout", "layout.jsx": "layout", "layout.ts": "layout", "layout.tsx": "layout", "page.js": "page", "page.jsx": "page", "page.ts": "page", "page.tsx": "page", "route.js": "route", "route.ts": "route", "loading.js": "loading", "loading.tsx": "loading", "not-found.js": "not-found", "not-found.tsx": "not-found", "error.js": "error", "error.tsx": "error", "docusaurus.config.js": "docusaurus", "docusaurus.config.ts": "docusaurus", "gatsby-config.js": "gatsby", "gatsby-node.js": "gatsby", "artisan": "laravel", "manage.py": "django", "app.py": "flask", "main.py": "python", "requirements.txt": "python"}, "folders": {"src": "folder-source", "source": "folder-source", "components": "folder-components", "component": "folder-components", "hooks": "folder-source", "utils": "folder-source", "util": "folder-source", "tests": "folder-test", "test": "folder-test", "__tests__": "folder-test", "config": "folder-config", "configs": "folder-config", "assets": "folder-assets", "public": "folder-assets", "images": "folder-assets", "img": "folder-assets", "docs": "folder-docs", "scripts": "folder-scripts", "crates": "folder-rust", "rust": "folder-rust", "src-tauri": "folder-rust", "tauri": "folder-rust", "node_modules": "folder-packages", ".git": "folder-git", ".github": "folder-git", ".vscode": "folder-config", "build": "folder-build", "dist": "folder-build", "database": "folder-database", "databases": "folder-database", "api": "folder-routes", "routes": "folder-routes", "router": "folder-routes", "stores": "folder-source", "store": "folder-source", "features": "folder-source", "ui": "folder-components", "pages": "folder-components", "app": "folder-components", "views": "folder-components", "view": "folder-components", "lib": "folder-source", "libs": "folder-source", "services": "folder-source", "service": "folder-source", "types": "folder-source", "typings": "folder-source", "styles": "folder-styles", "style": "folder-styles", "css": "folder-styles", "locales": "folder-locales", "i18n": "folder-locales", "terminal": "folder-scripts", "shell": "folder-scripts", "auth": "folder-security", "security": "folder-security", ".circleci": "folder-git", ".buildkite": "folder-git", ".docker": "folder-cloud", "docker": "folder-cloud", "k8s": "folder-cloud", "kubernetes": "folder-cloud", "helm": "folder-cloud", "cloud": "folder-cloud", ".firebase": "folder-database", "firebase": "folder-database", ".supabase": "folder-database", "supabase": "folder-database", "prisma": "folder-database", "cache": "folder-build", "logs": "folder-build", "log": "folder-build", "tmp": "folder-build", "temp": "folder-build", "mobile": "folder-mobile", "ios": "folder-mobile", "android": "folder-mobile", ".storybook": "folder-components", "storybook": "folder-components", "fixtures": "folder-test", "mocks": "folder-test", "mock": "folder-test", "generated": "folder-build", "gen": "folder-build", "benchmark": "folder-test", "benchmarks": "folder-test", "extensions": "folder-extensions", "extension": "folder-extensions", "themes": "folder-styles", "theme": "folder-styles", "ai": "folder-ai", ".agents": "folder-ai", "agents": "folder-ai", ".codex": "folder-ai", "codex": "folder-ai", ".claude": "folder-ai", "claude": "folder-ai", "packages": "folder-packages", "package": "folder-packages", "examples": "folder-docs", "example": "folder-docs", "playground": "folder-test", "play": "folder-test", "commands": "folder-scripts", "command": "folder-scripts", "plugins": "folder-extensions", "plugin": "folder-extensions", "workflows": "folder-git", "workflow": "folder-git", ".devcontainer": "folder-cloud", "devcontainer": "folder-cloud", "web": "folder-mobile", "www": "folder-mobile", "server": "folder-routes", "client": "folder-mobile", "shared": "folder-source", "common": "folder-source"}, "expandedFolders": {"src": "folder-source-open", "source": "folder-source-open", "components": "folder-components-open", "component": "folder-components-open", "hooks": "folder-source-open", "utils": "folder-source-open", "util": "folder-source-open", "tests": "folder-test-open", "test": "folder-test-open", "__tests__": "folder-test-open", "config": "folder-config-open", "configs": "folder-config-open", "assets": "folder-assets-open", "public": "folder-assets-open", "images": "folder-assets-open", "img": "folder-assets-open", "docs": "folder-docs-open", "scripts": "folder-scripts-open", "crates": "folder-rust-open", "rust": "folder-rust-open", "src-tauri": "folder-rust-open", "tauri": "folder-rust-open", "node_modules": "folder-packages-open", ".git": "folder-git-open", ".github": "folder-git-open", ".vscode": "folder-config-open", "build": "folder-build-open", "dist": "folder-build-open", "database": "folder-database-open", "databases": "folder-database-open", "api": "folder-routes-open", "routes": "folder-routes-open", "router": "folder-routes-open", "stores": "folder-source-open", "store": "folder-source-open", "features": "folder-source-open", "ui": "folder-components-open", "pages": "folder-components-open", "app": "folder-components-open", "views": "folder-components-open", "view": "folder-components-open", "lib": "folder-source-open", "libs": "folder-source-open", "services": "folder-source-open", "service": "folder-source-open", "types": "folder-source-open", "typings": "folder-source-open", "styles": "folder-styles-open", "style": "folder-styles-open", "css": "folder-styles-open", "locales": "folder-locales-open", "i18n": "folder-locales-open", "terminal": "folder-scripts-open", "shell": "folder-scripts-open", "auth": "folder-security-open", "security": "folder-security-open", ".circleci": "folder-git-open", ".buildkite": "folder-git-open", ".docker": "folder-cloud-open", "docker": "folder-cloud-open", "k8s": "folder-cloud-open", "kubernetes": "folder-cloud-open", "helm": "folder-cloud-open", "cloud": "folder-cloud-open", ".firebase": "folder-database-open", "firebase": "folder-database-open", ".supabase": "folder-database-open", "supabase": "folder-database-open", "prisma": "folder-database-open", "cache": "folder-build-open", "logs": "folder-build-open", "log": "folder-build-open", "tmp": "folder-build-open", "temp": "folder-build-open", "mobile": "folder-mobile-open", "ios": "folder-mobile-open", "android": "folder-mobile-open", ".storybook": "folder-components-open", "storybook": "folder-components-open", "fixtures": "folder-test-open", "mocks": "folder-test-open", "mock": "folder-test-open", "generated": "folder-build-open", "gen": "folder-build-open", "benchmark": "folder-test-open", "benchmarks": "folder-test-open", "extensions": "folder-extensions-open", "extension": "folder-extensions-open", "themes": "folder-styles-open", "theme": "folder-styles-open", "ai": "folder-ai-open", ".agents": "folder-ai-open", "agents": "folder-ai-open", ".codex": "folder-ai-open", "codex": "folder-ai-open", ".claude": "folder-ai-open", "claude": "folder-ai-open", "packages": "folder-packages-open", "package": "folder-packages-open", "examples": "folder-docs-open", "example": "folder-docs-open", "playground": "folder-test-open", "play": "folder-test-open", "commands": "folder-scripts-open", "command": "folder-scripts-open", "plugins": "folder-extensions-open", "plugin": "folder-extensions-open", "workflows": "folder-git-open", "workflow": "folder-git-open", ".devcontainer": "folder-cloud-open", "devcontainer": "folder-cloud-open", "web": "folder-mobile-open", "www": "folder-mobile-open", "server": "folder-routes-open", "client": "folder-mobile-open", "shared": "folder-source-open", "common": "folder-source-open"}} \ No newline at end of file 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/workbench/editor.rs b/linux/src/workbench/editor.rs index 3bdfbc82a..a8c31fab7 100644 --- a/linux/src/workbench/editor.rs +++ b/linux/src/workbench/editor.rs @@ -882,6 +882,10 @@ impl Render for EditorView { 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(); @@ -902,11 +906,6 @@ impl Render for EditorView { let is_active = self.active_tab_index == Some(idx); let title = tab.title.clone(); let is_dirty = tab.is_dirty; - let icon_name = if is_code_file(&title) { - IconName::FileCode - } else { - IconName::FileText - }; h_flex() .id(idx) @@ -926,13 +925,31 @@ impl Render for EditorView { cx.notify(); })) .child( - Icon::new(icon_name) - .size(px(14.0)) - .text_color(if is_active { - ThemeColors::accent_blue() + 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 { - ThemeColors::text_muted() - }), + 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() 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/mod.rs b/linux/src/workbench/mod.rs index 069f485e4..d8af16c5f 100644 --- a/linux/src/workbench/mod.rs +++ b/linux/src/workbench/mod.rs @@ -4,6 +4,7 @@ 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; @@ -33,6 +34,8 @@ 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}; diff --git a/linux/src/workbench/quick_open.rs b/linux/src/workbench/quick_open.rs index a9d7138fd..123e9ca2d 100644 --- a/linux/src/workbench/quick_open.rs +++ b/linux/src/workbench/quick_open.rs @@ -239,6 +239,10 @@ impl Render for QuickOpenModal { 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() @@ -412,13 +416,30 @@ impl Render for QuickOpenModal { row.hover(|h| h.bg(ThemeColors::accent())) }) .child( - Icon::new(IconName::FileText) - .size(px(14.0)) - .text_color(if is_selected { - ThemeColors::primary() - } else { - ThemeColors::muted_foreground() - }), + 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() diff --git a/linux/src/workbench/sidebar.rs b/linux/src/workbench/sidebar.rs index 630bfa696..8b7882287 100644 --- a/linux/src/workbench/sidebar.rs +++ b/linux/src/workbench/sidebar.rs @@ -800,6 +800,7 @@ impl SidebarView { let show_hidden = s.show_hidden_files_in_file_tree; let indent_size = s.file_tree_indent_size; let show_icons = s.show_file_icons_in_file_tree; + let is_dark = !crate::theme::ThemePalette::is_light(&settings::resolved_theme_id(s, false)); let query = self.tree_filter.trim().to_lowercase(); let mut visible_items = Vec::new(); @@ -895,9 +896,24 @@ impl SidebarView { .text_color(ThemeColors::text_muted()), ) .child( - Icon::new(IconName::FolderOpen) - .size(px(14.0)) - .text_color(ThemeColors::accent_blue()), + if let Some(arc) = crate::workbench::file_icon::folder_image( + &item.name, + item.is_expanded, + 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::FolderOpen) + .size(px(14.0)) + .text_color(ThemeColors::accent_blue()) + .into_any_element() + }, ) .into_any_element() } else { @@ -910,27 +926,55 @@ impl SidebarView { .text_color(ThemeColors::text_muted()), ) .child( - Icon::new(IconName::Folder) - .size(px(14.0)) - .text_color(ThemeColors::accent_blue()), + if let Some(arc) = crate::workbench::file_icon::folder_image( + &item.name, + item.is_expanded, + 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::Folder) + .size(px(14.0)) + .text_color(ThemeColors::accent_blue()) + .into_any_element() + }, ) .into_any_element() } } else { - let icon_name = if is_code_file(&item.name) { - IconName::FileCode - } else { - IconName::FileText - }; h_flex() .items_center() .gap_1() .child(div().w(px(12.0))) .when(show_icons, |row| { row.child( - Icon::new(icon_name) - .size(px(14.0)) - .text_color(ThemeColors::text_muted()), + if let Some(arc) = + crate::workbench::file_icon::file_image(&item.name, 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(&item.name) { + IconName::FileCode + } else { + IconName::FileText + }; + Icon::new(icon_name) + .size(px(14.0)) + .text_color(ThemeColors::text_muted()) + .into_any_element() + }, ) }) .into_any_element() From 9392c98ab54f67756aa189ff48cf9ee2b91e6be3 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 23:54:39 +0800 Subject: [PATCH 28/59] =?UTF-8?q?feat(linux):=20=E5=9B=BE=E6=A0=87?= =?UTF-8?q?=E5=88=87=20Windows=20=E9=BB=98=E8=AE=A4=20idea=20=E5=A5=97?= =?UTF-8?q?=E8=A3=85=E5=B9=B6=E5=90=AF=E7=94=A8=20tree-sitter=20=E9=AB=98?= =?UTF-8?q?=E4=BA=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- linux/Cargo.toml | 14 +++++++++++++- .../file-icons/icons/expui/fileTypes/Csharp.svg | 3 +++ .../icons/expui/fileTypes/Csharp_dark.svg | 3 +++ .../file-icons/icons/expui/fileTypes/archive.svg | 8 ++++++++ .../icons/expui/fileTypes/archive_dark.svg | 8 ++++++++ .../assets/file-icons/icons/expui/fileTypes/c.svg | 4 ++++ .../file-icons/icons/expui/fileTypes/c_dark.svg | 4 ++++ .../file-icons/icons/expui/fileTypes/cpp.svg | 7 +++++++ .../file-icons/icons/expui/fileTypes/cpp_dark.svg | 7 +++++++ .../file-icons/icons/expui/fileTypes/css.svg | 5 +++++ .../file-icons/icons/expui/fileTypes/css_dark.svg | 5 +++++ .../file-icons/icons/expui/fileTypes/csv.svg | 7 +++++++ .../file-icons/icons/expui/fileTypes/csv_dark.svg | 7 +++++++ .../file-icons/icons/expui/fileTypes/docker.svg | 5 +++++ .../icons/expui/fileTypes/docker_dark.svg | 5 +++++ .../icons/expui/fileTypes/editorConfig.svg | 4 ++++ .../icons/expui/fileTypes/editorConfig_dark.svg | 4 ++++ .../file-icons/icons/expui/fileTypes/font.svg | 5 +++++ .../file-icons/icons/expui/fileTypes/font_dark.svg | 5 +++++ .../file-icons/icons/expui/fileTypes/gitignore.svg | 4 ++++ .../file-icons/icons/expui/fileTypes/gradle.svg | 5 +++++ .../icons/expui/fileTypes/gradle_dark.svg | 5 +++++ .../file-icons/icons/expui/fileTypes/graphql.svg | 4 ++++ .../icons/expui/fileTypes/graphql_dark.svg | 4 ++++ .../file-icons/icons/expui/fileTypes/groovy.svg | 5 +++++ .../icons/expui/fileTypes/groovy_dark.svg | 5 +++++ .../assets/file-icons/icons/expui/fileTypes/h.svg | 6 ++++++ .../file-icons/icons/expui/fileTypes/h_dark.svg | 6 ++++++ .../file-icons/icons/expui/fileTypes/html.svg | 5 +++++ .../file-icons/icons/expui/fileTypes/html_dark.svg | 5 +++++ .../file-icons/icons/expui/fileTypes/http.svg | 6 ++++++ .../file-icons/icons/expui/fileTypes/image.svg | 6 ++++++ .../icons/expui/fileTypes/image_dark.svg | 6 ++++++ .../file-icons/icons/expui/fileTypes/java.svg | 6 ++++++ .../icons/expui/fileTypes/javaScript.svg | 6 ++++++ .../icons/expui/fileTypes/javaScript_dark.svg | 6 ++++++ .../file-icons/icons/expui/fileTypes/java_dark.svg | 6 ++++++ .../file-icons/icons/expui/fileTypes/json.svg | 5 +++++ .../file-icons/icons/expui/fileTypes/json_dark.svg | 5 +++++ .../file-icons/icons/expui/fileTypes/jsp.svg | 9 +++++++++ .../file-icons/icons/expui/fileTypes/jsp_dark.svg | 9 +++++++++ .../file-icons/icons/expui/fileTypes/markdown.svg | 5 +++++ .../icons/expui/fileTypes/markdown_dark.svg | 5 +++++ .../file-icons/icons/expui/fileTypes/maven.svg | 4 ++++ .../icons/expui/fileTypes/maven_dark.svg | 4 ++++ .../file-icons/icons/expui/fileTypes/patch.svg | 5 +++++ .../icons/expui/fileTypes/patch_dark.svg | 5 +++++ .../file-icons/icons/expui/fileTypes/perl.svg | 4 ++++ .../file-icons/icons/expui/fileTypes/perl_dark.svg | 4 ++++ .../icons/expui/fileTypes/powershell.svg | 7 +++++++ .../icons/expui/fileTypes/powershell_dark.svg | 7 +++++++ .../icons/expui/fileTypes/properties.svg | 4 ++++ .../icons/expui/fileTypes/properties_dark.svg | 4 ++++ .../file-icons/icons/expui/fileTypes/rst.svg | 8 ++++++++ .../file-icons/icons/expui/fileTypes/rst_dark.svg | 8 ++++++++ .../file-icons/icons/expui/fileTypes/shell.svg | 6 ++++++ .../icons/expui/fileTypes/shell_dark.svg | 6 ++++++ .../file-icons/icons/expui/fileTypes/sql.svg | 10 ++++++++++ .../file-icons/icons/expui/fileTypes/sql_dark.svg | 10 ++++++++++ .../file-icons/icons/expui/fileTypes/swiftLang.svg | 3 +++ .../icons/expui/fileTypes/swiftLang_dark.svg | 3 +++ .../file-icons/icons/expui/fileTypes/terraform.svg | 6 ++++++ .../icons/expui/fileTypes/terraform_dark.svg | 6 ++++++ .../file-icons/icons/expui/fileTypes/text.svg | 7 +++++++ .../file-icons/icons/expui/fileTypes/text_dark.svg | 7 +++++++ .../file-icons/icons/expui/fileTypes/toml.svg | 5 +++++ .../file-icons/icons/expui/fileTypes/toml_dark.svg | 5 +++++ .../icons/expui/fileTypes/typeScript.svg | 6 ++++++ .../icons/expui/fileTypes/typeScript_dark.svg | 6 ++++++ .../file-icons/icons/expui/fileTypes/vue.svg | 6 ++++++ .../file-icons/icons/expui/fileTypes/wsdl.svg | 8 ++++++++ .../file-icons/icons/expui/fileTypes/wsdl_dark.svg | 8 ++++++++ .../file-icons/icons/expui/fileTypes/xhtml.svg | 5 +++++ .../icons/expui/fileTypes/xhtml_dark.svg | 5 +++++ .../file-icons/icons/expui/fileTypes/xml.svg | 4 ++++ .../file-icons/icons/expui/fileTypes/xml_dark.svg | 4 ++++ .../file-icons/icons/expui/fileTypes/xsd.svg | 9 +++++++++ .../file-icons/icons/expui/fileTypes/xsd_dark.svg | 9 +++++++++ .../file-icons/icons/expui/fileTypes/yaml.svg | 7 +++++++ .../file-icons/icons/expui/fileTypes/yaml_dark.svg | 7 +++++++ .../file-icons/icons/expui/nodes/annotation.svg | 5 +++++ .../icons/expui/nodes/annotation_dark.svg | 5 +++++ .../assets/file-icons/icons/expui/nodes/class.svg | 5 +++++ .../file-icons/icons/expui/nodes/class_dark.svg | 5 +++++ linux/assets/file-icons/icons/expui/nodes/enum.svg | 5 +++++ .../file-icons/icons/expui/nodes/enum_dark.svg | 5 +++++ .../file-icons/icons/expui/nodes/exception.svg | 5 +++++ .../icons/expui/nodes/exception_dark.svg | 5 +++++ .../assets/file-icons/icons/expui/nodes/folder.svg | 4 ++++ .../file-icons/icons/expui/nodes/folder_dark.svg | 4 ++++ .../file-icons/icons/expui/nodes/interface.svg | 5 +++++ .../icons/expui/nodes/interface_dark.svg | 5 +++++ .../file-icons/icons/expui/nodes/package.svg | 5 +++++ .../file-icons/icons/expui/nodes/package_dark.svg | 5 +++++ .../assets/file-icons/icons/expui/nodes/record.svg | 5 +++++ .../file-icons/icons/expui/nodes/record_dark.svg | 5 +++++ .../file-icons/icons/expui/nodes/resourcesRoot.svg | 8 ++++++++ .../icons/expui/nodes/resourcesRoot_dark.svg | 8 ++++++++ .../file-icons/icons/expui/nodes/sourceRoot.svg | 4 ++++ .../icons/expui/nodes/sourceRoot_dark.svg | 4 ++++ .../icons/expui/nodes/testResourcesRoot.svg | 8 ++++++++ .../icons/expui/nodes/testResourcesRoot_dark.svg | 8 ++++++++ .../file-icons/icons/expui/nodes/testRoot.svg | 4 ++++ .../file-icons/icons/expui/nodes/testRoot_dark.svg | 4 ++++ linux/assets/file-icons/icons/files/adobe.svg | 7 ------- linux/assets/file-icons/icons/files/agents.svg | 7 ------- linux/assets/file-icons/icons/files/android.svg | 7 ------- linux/assets/file-icons/icons/files/angular.svg | 7 ------- linux/assets/file-icons/icons/files/ansible.svg | 7 ------- linux/assets/file-icons/icons/files/apple.svg | 7 ------- linux/assets/file-icons/icons/files/archive.svg | 7 ------- linux/assets/file-icons/icons/files/arduino.svg | 7 ------- linux/assets/file-icons/icons/files/astro.svg | 7 ------- linux/assets/file-icons/icons/files/audio.svg | 7 ------- linux/assets/file-icons/icons/files/authors.svg | 7 ------- linux/assets/file-icons/icons/files/babel.svg | 7 ------- linux/assets/file-icons/icons/files/biome.svg | 7 ------- linux/assets/file-icons/icons/files/bitbucket.svg | 7 ------- linux/assets/file-icons/icons/files/blender.svg | 7 ------- linux/assets/file-icons/icons/files/bun.svg | 7 ------- linux/assets/file-icons/icons/files/c.svg | 7 ------- .../assets/file-icons/icons/files/certificate.svg | 7 ------- linux/assets/file-icons/icons/files/changelog.svg | 7 ------- linux/assets/file-icons/icons/files/claude.svg | 7 ------- linux/assets/file-icons/icons/files/cline.svg | 7 ------- linux/assets/file-icons/icons/files/clojure.svg | 7 ------- linux/assets/file-icons/icons/files/cloudflare.svg | 7 ------- linux/assets/file-icons/icons/files/cmake.svg | 7 ------- linux/assets/file-icons/icons/files/codex.svg | 7 ------- linux/assets/file-icons/icons/files/commitlint.svg | 7 ------- linux/assets/file-icons/icons/files/config.svg | 7 ------- linux/assets/file-icons/icons/files/copilot.svg | 7 ------- linux/assets/file-icons/icons/files/cpp.svg | 7 ------- linux/assets/file-icons/icons/files/csharp.svg | 7 ------- linux/assets/file-icons/icons/files/cspell.svg | 7 ------- linux/assets/file-icons/icons/files/css.svg | 7 ------- linux/assets/file-icons/icons/files/csv.svg | 7 ------- linux/assets/file-icons/icons/files/cursor.svg | 7 ------- linux/assets/file-icons/icons/files/cypress.svg | 7 ------- linux/assets/file-icons/icons/files/dart.svg | 7 ------- linux/assets/file-icons/icons/files/database.svg | 7 ------- linux/assets/file-icons/icons/files/deno.svg | 7 ------- linux/assets/file-icons/icons/files/dependabot.svg | 7 ------- .../assets/file-icons/icons/files/devcontainer.svg | 7 ------- linux/assets/file-icons/icons/files/diff.svg | 7 ------- linux/assets/file-icons/icons/files/django.svg | 7 ------- .../file-icons/icons/files/docker-compose.svg | 7 ------- linux/assets/file-icons/icons/files/docker.svg | 7 ------- linux/assets/file-icons/icons/files/document.svg | 7 ------- linux/assets/file-icons/icons/files/docusaurus.svg | 7 ------- linux/assets/file-icons/icons/files/drawio.svg | 7 ------- linux/assets/file-icons/icons/files/drizzle.svg | 7 ------- .../assets/file-icons/icons/files/editorconfig.svg | 7 ------- linux/assets/file-icons/icons/files/electron.svg | 7 ------- linux/assets/file-icons/icons/files/elixir.svg | 7 ------- linux/assets/file-icons/icons/files/env.svg | 7 ------- linux/assets/file-icons/icons/files/erlang.svg | 7 ------- linux/assets/file-icons/icons/files/error.svg | 7 ------- linux/assets/file-icons/icons/files/eslint.svg | 7 ------- linux/assets/file-icons/icons/files/excalidraw.svg | 7 ------- linux/assets/file-icons/icons/files/fastapi.svg | 7 ------- linux/assets/file-icons/icons/files/figma.svg | 7 ------- linux/assets/file-icons/icons/files/file.svg | 7 ------- linux/assets/file-icons/icons/files/firebase.svg | 7 ------- linux/assets/file-icons/icons/files/flask.svg | 7 ------- linux/assets/file-icons/icons/files/font.svg | 7 ------- linux/assets/file-icons/icons/files/gatsby.svg | 7 ------- linux/assets/file-icons/icons/files/gemini.svg | 7 ------- linux/assets/file-icons/icons/files/git.svg | 7 ------- .../file-icons/icons/files/github-actions.svg | 7 ------- linux/assets/file-icons/icons/files/github.svg | 7 ------- linux/assets/file-icons/icons/files/gitlab.svg | 7 ------- linux/assets/file-icons/icons/files/go.svg | 7 ------- linux/assets/file-icons/icons/files/gradle.svg | 7 ------- .../file-icons/icons/files/graphql-schema.svg | 7 ------- linux/assets/file-icons/icons/files/graphql.svg | 7 ------- linux/assets/file-icons/icons/files/haskell.svg | 7 ------- linux/assets/file-icons/icons/files/helm.svg | 7 ------- linux/assets/file-icons/icons/files/html.svg | 7 ------- linux/assets/file-icons/icons/files/http.svg | 7 ------- linux/assets/file-icons/icons/files/hurl.svg | 7 ------- linux/assets/file-icons/icons/files/image.svg | 7 ------- linux/assets/file-icons/icons/files/index-js.svg | 7 ------- linux/assets/file-icons/icons/files/index-ts.svg | 7 ------- linux/assets/file-icons/icons/files/java.svg | 7 ------- linux/assets/file-icons/icons/files/javascript.svg | 7 ------- linux/assets/file-icons/icons/files/jenkins.svg | 7 ------- linux/assets/file-icons/icons/files/jest.svg | 7 ------- linux/assets/file-icons/icons/files/jsconfig.svg | 7 ------- linux/assets/file-icons/icons/files/json.svg | 7 ------- linux/assets/file-icons/icons/files/julia.svg | 7 ------- linux/assets/file-icons/icons/files/key.svg | 7 ------- linux/assets/file-icons/icons/files/kotlin.svg | 7 ------- linux/assets/file-icons/icons/files/kubernetes.svg | 7 ------- linux/assets/file-icons/icons/files/laravel.svg | 7 ------- linux/assets/file-icons/icons/files/layout.svg | 7 ------- linux/assets/file-icons/icons/files/license.svg | 7 ------- linux/assets/file-icons/icons/files/lintstaged.svg | 7 ------- linux/assets/file-icons/icons/files/linux.svg | 7 ------- linux/assets/file-icons/icons/files/lit.svg | 7 ------- linux/assets/file-icons/icons/files/lithe.svg | 7 ------- linux/assets/file-icons/icons/files/loading.svg | 7 ------- linux/assets/file-icons/icons/files/lock.svg | 7 ------- linux/assets/file-icons/icons/files/log.svg | 7 ------- linux/assets/file-icons/icons/files/lua.svg | 7 ------- linux/assets/file-icons/icons/files/makefile.svg | 7 ------- linux/assets/file-icons/icons/files/markdown.svg | 7 ------- .../assets/file-icons/icons/files/markdownlint.svg | 7 ------- linux/assets/file-icons/icons/files/maven.svg | 7 ------- linux/assets/file-icons/icons/files/mcp.svg | 7 ------- linux/assets/file-icons/icons/files/mermaid.svg | 7 ------- linux/assets/file-icons/icons/files/mongo.svg | 7 ------- linux/assets/file-icons/icons/files/netlify.svg | 7 ------- linux/assets/file-icons/icons/files/next.svg | 7 ------- linux/assets/file-icons/icons/files/nginx.svg | 7 ------- linux/assets/file-icons/icons/files/nim.svg | 7 ------- linux/assets/file-icons/icons/files/nix.svg | 7 ------- linux/assets/file-icons/icons/files/node.svg | 7 ------- linux/assets/file-icons/icons/files/not-found.svg | 7 ------- linux/assets/file-icons/icons/files/notebook.svg | 7 ------- linux/assets/file-icons/icons/files/npm.svg | 7 ------- linux/assets/file-icons/icons/files/nuxt.svg | 7 ------- linux/assets/file-icons/icons/files/nx.svg | 7 ------- linux/assets/file-icons/icons/files/ocaml.svg | 7 ------- linux/assets/file-icons/icons/files/package.svg | 7 ------- linux/assets/file-icons/icons/files/page.svg | 7 ------- linux/assets/file-icons/icons/files/patch.svg | 7 ------- linux/assets/file-icons/icons/files/pdf.svg | 7 ------- linux/assets/file-icons/icons/files/perl.svg | 7 ------- linux/assets/file-icons/icons/files/php.svg | 7 ------- linux/assets/file-icons/icons/files/playwright.svg | 7 ------- linux/assets/file-icons/icons/files/pnpm.svg | 7 ------- linux/assets/file-icons/icons/files/postgres.svg | 7 ------- linux/assets/file-icons/icons/files/powerpoint.svg | 7 ------- linux/assets/file-icons/icons/files/prettier.svg | 7 ------- linux/assets/file-icons/icons/files/prisma.svg | 7 ------- linux/assets/file-icons/icons/files/proto.svg | 7 ------- linux/assets/file-icons/icons/files/python.svg | 7 ------- linux/assets/file-icons/icons/files/qwik.svg | 7 ------- linux/assets/file-icons/icons/files/r.svg | 7 ------- linux/assets/file-icons/icons/files/react.svg | 7 ------- linux/assets/file-icons/icons/files/redis.svg | 7 ------- linux/assets/file-icons/icons/files/remix.svg | 7 ------- linux/assets/file-icons/icons/files/renovate.svg | 7 ------- linux/assets/file-icons/icons/files/rescript.svg | 7 ------- linux/assets/file-icons/icons/files/rollup.svg | 7 ------- linux/assets/file-icons/icons/files/route.svg | 7 ------- linux/assets/file-icons/icons/files/rspack.svg | 7 ------- linux/assets/file-icons/icons/files/ruby.svg | 7 ------- linux/assets/file-icons/icons/files/rust.svg | 7 ------- linux/assets/file-icons/icons/files/sass.svg | 7 ------- linux/assets/file-icons/icons/files/scala.svg | 7 ------- linux/assets/file-icons/icons/files/security.svg | 7 ------- linux/assets/file-icons/icons/files/shell.svg | 7 ------- linux/assets/file-icons/icons/files/sketch.svg | 7 ------- linux/assets/file-icons/icons/files/solid.svg | 7 ------- linux/assets/file-icons/icons/files/solidity.svg | 7 ------- .../assets/file-icons/icons/files/spreadsheet.svg | 7 ------- linux/assets/file-icons/icons/files/sql.svg | 7 ------- linux/assets/file-icons/icons/files/storybook.svg | 7 ------- linux/assets/file-icons/icons/files/stylelint.svg | 7 ------- linux/assets/file-icons/icons/files/supabase.svg | 7 ------- linux/assets/file-icons/icons/files/svelte.svg | 7 ------- linux/assets/file-icons/icons/files/svg.svg | 7 ------- linux/assets/file-icons/icons/files/swc.svg | 7 ------- linux/assets/file-icons/icons/files/swift.svg | 7 ------- linux/assets/file-icons/icons/files/tailwind.svg | 7 ------- linux/assets/file-icons/icons/files/tauri.svg | 7 ------- linux/assets/file-icons/icons/files/terraform.svg | 7 ------- linux/assets/file-icons/icons/files/test.svg | 7 ------- linux/assets/file-icons/icons/files/text.svg | 7 ------- linux/assets/file-icons/icons/files/toml.svg | 7 ------- linux/assets/file-icons/icons/files/turborepo.svg | 7 ------- linux/assets/file-icons/icons/files/typescript.svg | 7 ------- .../file-icons/icons/files/vercel-config.svg | 7 ------- linux/assets/file-icons/icons/files/vercel.svg | 7 ------- linux/assets/file-icons/icons/files/video.svg | 7 ------- linux/assets/file-icons/icons/files/vite.svg | 7 ------- linux/assets/file-icons/icons/files/vitest.svg | 7 ------- linux/assets/file-icons/icons/files/vue.svg | 7 ------- linux/assets/file-icons/icons/files/warning.svg | 7 ------- linux/assets/file-icons/icons/files/wasm.svg | 7 ------- linux/assets/file-icons/icons/files/webpack.svg | 7 ------- linux/assets/file-icons/icons/files/windows.svg | 7 ------- linux/assets/file-icons/icons/files/word.svg | 7 ------- linux/assets/file-icons/icons/files/xcode.svg | 7 ------- linux/assets/file-icons/icons/files/xml.svg | 7 ------- linux/assets/file-icons/icons/files/yaml.svg | 7 ------- linux/assets/file-icons/icons/files/yarn.svg | 7 ------- linux/assets/file-icons/icons/files/zig.svg | 7 ------- .../file-icons/icons/folders/folder-ai-open.svg | 5 ----- .../assets/file-icons/icons/folders/folder-ai.svg | 5 ----- .../icons/folders/folder-assets-open.svg | 5 ----- .../file-icons/icons/folders/folder-assets.svg | 5 ----- .../file-icons/icons/folders/folder-build-open.svg | 5 ----- .../file-icons/icons/folders/folder-build.svg | 5 ----- .../file-icons/icons/folders/folder-cloud-open.svg | 5 ----- .../file-icons/icons/folders/folder-cloud.svg | 5 ----- .../icons/folders/folder-components-open.svg | 5 ----- .../file-icons/icons/folders/folder-components.svg | 5 ----- .../icons/folders/folder-config-open.svg | 5 ----- .../file-icons/icons/folders/folder-config.svg | 5 ----- .../icons/folders/folder-database-open.svg | 5 ----- .../file-icons/icons/folders/folder-database.svg | 5 ----- .../file-icons/icons/folders/folder-docs-open.svg | 5 ----- .../file-icons/icons/folders/folder-docs.svg | 5 ----- .../icons/folders/folder-extensions-open.svg | 5 ----- .../file-icons/icons/folders/folder-extensions.svg | 5 ----- .../file-icons/icons/folders/folder-git-open.svg | 5 ----- .../assets/file-icons/icons/folders/folder-git.svg | 5 ----- .../icons/folders/folder-locales-open.svg | 5 ----- .../file-icons/icons/folders/folder-locales.svg | 5 ----- .../icons/folders/folder-mobile-open.svg | 5 ----- .../file-icons/icons/folders/folder-mobile.svg | 5 ----- .../file-icons/icons/folders/folder-open.svg | 4 ---- .../icons/folders/folder-packages-open.svg | 5 ----- .../file-icons/icons/folders/folder-packages.svg | 5 ----- .../icons/folders/folder-routes-open.svg | 5 ----- .../file-icons/icons/folders/folder-routes.svg | 5 ----- .../file-icons/icons/folders/folder-rust-open.svg | 5 ----- .../file-icons/icons/folders/folder-rust.svg | 5 ----- .../icons/folders/folder-scripts-open.svg | 5 ----- .../file-icons/icons/folders/folder-scripts.svg | 5 ----- .../icons/folders/folder-security-open.svg | 5 ----- .../file-icons/icons/folders/folder-security.svg | 5 ----- .../icons/folders/folder-source-open.svg | 5 ----- .../file-icons/icons/folders/folder-source.svg | 5 ----- .../icons/folders/folder-styles-open.svg | 5 ----- .../file-icons/icons/folders/folder-styles.svg | 5 ----- .../file-icons/icons/folders/folder-test-open.svg | 5 ----- .../file-icons/icons/folders/folder-test.svg | 5 ----- linux/assets/file-icons/icons/folders/folder.svg | 4 ---- .../assets/file-icons/icons/light/files/adobe.svg | 7 ------- .../assets/file-icons/icons/light/files/agents.svg | 7 ------- .../file-icons/icons/light/files/android.svg | 7 ------- .../file-icons/icons/light/files/angular.svg | 7 ------- .../file-icons/icons/light/files/ansible.svg | 7 ------- .../assets/file-icons/icons/light/files/apple.svg | 7 ------- .../file-icons/icons/light/files/archive.svg | 7 ------- .../file-icons/icons/light/files/arduino.svg | 7 ------- .../assets/file-icons/icons/light/files/astro.svg | 7 ------- .../assets/file-icons/icons/light/files/audio.svg | 7 ------- .../file-icons/icons/light/files/authors.svg | 7 ------- .../assets/file-icons/icons/light/files/babel.svg | 7 ------- .../assets/file-icons/icons/light/files/biome.svg | 7 ------- .../file-icons/icons/light/files/bitbucket.svg | 7 ------- .../file-icons/icons/light/files/blender.svg | 7 ------- linux/assets/file-icons/icons/light/files/bun.svg | 7 ------- linux/assets/file-icons/icons/light/files/c.svg | 7 ------- .../file-icons/icons/light/files/certificate.svg | 7 ------- .../file-icons/icons/light/files/changelog.svg | 7 ------- .../assets/file-icons/icons/light/files/claude.svg | 7 ------- .../assets/file-icons/icons/light/files/cline.svg | 7 ------- .../file-icons/icons/light/files/clojure.svg | 7 ------- .../file-icons/icons/light/files/cloudflare.svg | 7 ------- .../assets/file-icons/icons/light/files/cmake.svg | 7 ------- .../assets/file-icons/icons/light/files/codex.svg | 7 ------- .../file-icons/icons/light/files/commitlint.svg | 7 ------- .../assets/file-icons/icons/light/files/config.svg | 7 ------- .../file-icons/icons/light/files/copilot.svg | 7 ------- linux/assets/file-icons/icons/light/files/cpp.svg | 7 ------- .../assets/file-icons/icons/light/files/csharp.svg | 7 ------- .../assets/file-icons/icons/light/files/cspell.svg | 7 ------- linux/assets/file-icons/icons/light/files/css.svg | 7 ------- linux/assets/file-icons/icons/light/files/csv.svg | 7 ------- .../assets/file-icons/icons/light/files/cursor.svg | 7 ------- .../file-icons/icons/light/files/cypress.svg | 7 ------- linux/assets/file-icons/icons/light/files/dart.svg | 7 ------- .../file-icons/icons/light/files/database.svg | 7 ------- linux/assets/file-icons/icons/light/files/deno.svg | 7 ------- .../file-icons/icons/light/files/dependabot.svg | 7 ------- .../file-icons/icons/light/files/devcontainer.svg | 7 ------- linux/assets/file-icons/icons/light/files/diff.svg | 7 ------- .../assets/file-icons/icons/light/files/django.svg | 7 ------- .../icons/light/files/docker-compose.svg | 7 ------- .../assets/file-icons/icons/light/files/docker.svg | 7 ------- .../file-icons/icons/light/files/document.svg | 7 ------- .../file-icons/icons/light/files/docusaurus.svg | 7 ------- .../assets/file-icons/icons/light/files/drawio.svg | 7 ------- .../file-icons/icons/light/files/drizzle.svg | 7 ------- .../file-icons/icons/light/files/editorconfig.svg | 7 ------- .../file-icons/icons/light/files/electron.svg | 7 ------- .../assets/file-icons/icons/light/files/elixir.svg | 7 ------- linux/assets/file-icons/icons/light/files/env.svg | 7 ------- .../assets/file-icons/icons/light/files/erlang.svg | 7 ------- .../assets/file-icons/icons/light/files/error.svg | 7 ------- .../assets/file-icons/icons/light/files/eslint.svg | 7 ------- .../file-icons/icons/light/files/excalidraw.svg | 7 ------- .../file-icons/icons/light/files/fastapi.svg | 7 ------- .../assets/file-icons/icons/light/files/figma.svg | 7 ------- linux/assets/file-icons/icons/light/files/file.svg | 7 ------- .../file-icons/icons/light/files/firebase.svg | 7 ------- .../assets/file-icons/icons/light/files/flask.svg | 7 ------- linux/assets/file-icons/icons/light/files/font.svg | 7 ------- .../assets/file-icons/icons/light/files/gatsby.svg | 7 ------- .../assets/file-icons/icons/light/files/gemini.svg | 7 ------- linux/assets/file-icons/icons/light/files/git.svg | 7 ------- .../icons/light/files/github-actions.svg | 7 ------- .../assets/file-icons/icons/light/files/github.svg | 7 ------- .../assets/file-icons/icons/light/files/gitlab.svg | 7 ------- linux/assets/file-icons/icons/light/files/go.svg | 7 ------- .../assets/file-icons/icons/light/files/gradle.svg | 7 ------- .../icons/light/files/graphql-schema.svg | 7 ------- .../file-icons/icons/light/files/graphql.svg | 7 ------- .../file-icons/icons/light/files/haskell.svg | 7 ------- linux/assets/file-icons/icons/light/files/helm.svg | 7 ------- linux/assets/file-icons/icons/light/files/html.svg | 7 ------- linux/assets/file-icons/icons/light/files/http.svg | 7 ------- linux/assets/file-icons/icons/light/files/hurl.svg | 7 ------- .../assets/file-icons/icons/light/files/image.svg | 7 ------- .../file-icons/icons/light/files/index-js.svg | 7 ------- .../file-icons/icons/light/files/index-ts.svg | 7 ------- linux/assets/file-icons/icons/light/files/java.svg | 7 ------- .../file-icons/icons/light/files/javascript.svg | 7 ------- .../file-icons/icons/light/files/jenkins.svg | 7 ------- linux/assets/file-icons/icons/light/files/jest.svg | 7 ------- .../file-icons/icons/light/files/jsconfig.svg | 7 ------- linux/assets/file-icons/icons/light/files/json.svg | 7 ------- .../assets/file-icons/icons/light/files/julia.svg | 7 ------- linux/assets/file-icons/icons/light/files/key.svg | 7 ------- .../assets/file-icons/icons/light/files/kotlin.svg | 7 ------- .../file-icons/icons/light/files/kubernetes.svg | 7 ------- .../file-icons/icons/light/files/laravel.svg | 7 ------- .../assets/file-icons/icons/light/files/layout.svg | 7 ------- .../file-icons/icons/light/files/license.svg | 7 ------- .../file-icons/icons/light/files/lintstaged.svg | 7 ------- .../assets/file-icons/icons/light/files/linux.svg | 7 ------- linux/assets/file-icons/icons/light/files/lit.svg | 7 ------- .../assets/file-icons/icons/light/files/lithe.svg | 7 ------- .../file-icons/icons/light/files/loading.svg | 7 ------- linux/assets/file-icons/icons/light/files/lock.svg | 7 ------- linux/assets/file-icons/icons/light/files/log.svg | 7 ------- linux/assets/file-icons/icons/light/files/lua.svg | 7 ------- .../file-icons/icons/light/files/makefile.svg | 7 ------- .../file-icons/icons/light/files/markdown.svg | 7 ------- .../file-icons/icons/light/files/markdownlint.svg | 7 ------- .../assets/file-icons/icons/light/files/maven.svg | 7 ------- linux/assets/file-icons/icons/light/files/mcp.svg | 7 ------- .../file-icons/icons/light/files/mermaid.svg | 7 ------- .../assets/file-icons/icons/light/files/mongo.svg | 7 ------- .../file-icons/icons/light/files/netlify.svg | 7 ------- linux/assets/file-icons/icons/light/files/next.svg | 7 ------- .../assets/file-icons/icons/light/files/nginx.svg | 7 ------- linux/assets/file-icons/icons/light/files/nim.svg | 7 ------- linux/assets/file-icons/icons/light/files/nix.svg | 7 ------- linux/assets/file-icons/icons/light/files/node.svg | 7 ------- .../file-icons/icons/light/files/not-found.svg | 7 ------- .../file-icons/icons/light/files/notebook.svg | 7 ------- linux/assets/file-icons/icons/light/files/npm.svg | 7 ------- linux/assets/file-icons/icons/light/files/nuxt.svg | 7 ------- linux/assets/file-icons/icons/light/files/nx.svg | 7 ------- .../assets/file-icons/icons/light/files/ocaml.svg | 7 ------- .../file-icons/icons/light/files/package.svg | 7 ------- linux/assets/file-icons/icons/light/files/page.svg | 7 ------- .../assets/file-icons/icons/light/files/patch.svg | 7 ------- linux/assets/file-icons/icons/light/files/pdf.svg | 7 ------- linux/assets/file-icons/icons/light/files/perl.svg | 7 ------- linux/assets/file-icons/icons/light/files/php.svg | 7 ------- .../file-icons/icons/light/files/playwright.svg | 7 ------- linux/assets/file-icons/icons/light/files/pnpm.svg | 7 ------- .../file-icons/icons/light/files/postgres.svg | 7 ------- .../file-icons/icons/light/files/powerpoint.svg | 7 ------- .../file-icons/icons/light/files/prettier.svg | 7 ------- .../assets/file-icons/icons/light/files/prisma.svg | 7 ------- .../assets/file-icons/icons/light/files/proto.svg | 7 ------- .../assets/file-icons/icons/light/files/python.svg | 7 ------- linux/assets/file-icons/icons/light/files/qwik.svg | 7 ------- linux/assets/file-icons/icons/light/files/r.svg | 7 ------- .../assets/file-icons/icons/light/files/react.svg | 7 ------- .../assets/file-icons/icons/light/files/redis.svg | 7 ------- .../assets/file-icons/icons/light/files/remix.svg | 7 ------- .../file-icons/icons/light/files/renovate.svg | 7 ------- .../file-icons/icons/light/files/rescript.svg | 7 ------- .../assets/file-icons/icons/light/files/rollup.svg | 7 ------- .../assets/file-icons/icons/light/files/route.svg | 7 ------- .../assets/file-icons/icons/light/files/rspack.svg | 7 ------- linux/assets/file-icons/icons/light/files/ruby.svg | 7 ------- linux/assets/file-icons/icons/light/files/rust.svg | 7 ------- linux/assets/file-icons/icons/light/files/sass.svg | 7 ------- .../assets/file-icons/icons/light/files/scala.svg | 7 ------- .../file-icons/icons/light/files/security.svg | 7 ------- .../assets/file-icons/icons/light/files/shell.svg | 7 ------- .../assets/file-icons/icons/light/files/sketch.svg | 7 ------- .../assets/file-icons/icons/light/files/solid.svg | 7 ------- .../file-icons/icons/light/files/solidity.svg | 7 ------- .../file-icons/icons/light/files/spreadsheet.svg | 7 ------- linux/assets/file-icons/icons/light/files/sql.svg | 7 ------- .../file-icons/icons/light/files/storybook.svg | 7 ------- .../file-icons/icons/light/files/stylelint.svg | 7 ------- .../file-icons/icons/light/files/supabase.svg | 7 ------- .../assets/file-icons/icons/light/files/svelte.svg | 7 ------- linux/assets/file-icons/icons/light/files/svg.svg | 7 ------- linux/assets/file-icons/icons/light/files/swc.svg | 7 ------- .../assets/file-icons/icons/light/files/swift.svg | 7 ------- .../file-icons/icons/light/files/tailwind.svg | 7 ------- .../assets/file-icons/icons/light/files/tauri.svg | 7 ------- .../file-icons/icons/light/files/terraform.svg | 7 ------- linux/assets/file-icons/icons/light/files/test.svg | 7 ------- linux/assets/file-icons/icons/light/files/text.svg | 7 ------- linux/assets/file-icons/icons/light/files/toml.svg | 7 ------- .../file-icons/icons/light/files/turborepo.svg | 7 ------- .../file-icons/icons/light/files/typescript.svg | 7 ------- .../file-icons/icons/light/files/vercel-config.svg | 7 ------- .../assets/file-icons/icons/light/files/vercel.svg | 7 ------- .../assets/file-icons/icons/light/files/video.svg | 7 ------- linux/assets/file-icons/icons/light/files/vite.svg | 7 ------- .../assets/file-icons/icons/light/files/vitest.svg | 7 ------- linux/assets/file-icons/icons/light/files/vue.svg | 7 ------- .../file-icons/icons/light/files/warning.svg | 7 ------- linux/assets/file-icons/icons/light/files/wasm.svg | 7 ------- .../file-icons/icons/light/files/webpack.svg | 7 ------- .../file-icons/icons/light/files/windows.svg | 7 ------- linux/assets/file-icons/icons/light/files/word.svg | 7 ------- .../assets/file-icons/icons/light/files/xcode.svg | 7 ------- linux/assets/file-icons/icons/light/files/xml.svg | 7 ------- linux/assets/file-icons/icons/light/files/yaml.svg | 7 ------- linux/assets/file-icons/icons/light/files/yarn.svg | 7 ------- linux/assets/file-icons/icons/light/files/zig.svg | 7 ------- .../icons/light/folders/folder-ai-open.svg | 5 ----- .../file-icons/icons/light/folders/folder-ai.svg | 5 ----- .../icons/light/folders/folder-assets-open.svg | 5 ----- .../icons/light/folders/folder-assets.svg | 5 ----- .../icons/light/folders/folder-build-open.svg | 5 ----- .../icons/light/folders/folder-build.svg | 5 ----- .../icons/light/folders/folder-cloud-open.svg | 5 ----- .../icons/light/folders/folder-cloud.svg | 5 ----- .../icons/light/folders/folder-components-open.svg | 5 ----- .../icons/light/folders/folder-components.svg | 5 ----- .../icons/light/folders/folder-config-open.svg | 5 ----- .../icons/light/folders/folder-config.svg | 5 ----- .../icons/light/folders/folder-database-open.svg | 5 ----- .../icons/light/folders/folder-database.svg | 5 ----- .../icons/light/folders/folder-docs-open.svg | 5 ----- .../file-icons/icons/light/folders/folder-docs.svg | 5 ----- .../icons/light/folders/folder-extensions-open.svg | 5 ----- .../icons/light/folders/folder-extensions.svg | 5 ----- .../icons/light/folders/folder-git-open.svg | 5 ----- .../file-icons/icons/light/folders/folder-git.svg | 5 ----- .../icons/light/folders/folder-locales-open.svg | 5 ----- .../icons/light/folders/folder-locales.svg | 5 ----- .../icons/light/folders/folder-mobile-open.svg | 5 ----- .../icons/light/folders/folder-mobile.svg | 5 ----- .../file-icons/icons/light/folders/folder-open.svg | 4 ---- .../icons/light/folders/folder-packages-open.svg | 5 ----- .../icons/light/folders/folder-packages.svg | 5 ----- .../icons/light/folders/folder-routes-open.svg | 5 ----- .../icons/light/folders/folder-routes.svg | 5 ----- .../icons/light/folders/folder-rust-open.svg | 5 ----- .../file-icons/icons/light/folders/folder-rust.svg | 5 ----- .../icons/light/folders/folder-scripts-open.svg | 5 ----- .../icons/light/folders/folder-scripts.svg | 5 ----- .../icons/light/folders/folder-security-open.svg | 5 ----- .../icons/light/folders/folder-security.svg | 5 ----- .../icons/light/folders/folder-source-open.svg | 5 ----- .../icons/light/folders/folder-source.svg | 5 ----- .../icons/light/folders/folder-styles-open.svg | 5 ----- .../icons/light/folders/folder-styles.svg | 5 ----- .../icons/light/folders/folder-test-open.svg | 5 ----- .../file-icons/icons/light/folders/folder-test.svg | 5 ----- .../file-icons/icons/light/folders/folder.svg | 4 ---- linux/assets/file-icons/map.json | 2 +- linux/src/workbench/editor.rs | 7 +++++++ 562 files changed, 599 insertions(+), 3022 deletions(-) create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/Csharp.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/Csharp_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/archive.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/archive_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/c.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/c_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/cpp.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/cpp_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/css.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/css_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/csv.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/csv_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/docker.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/docker_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/editorConfig.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/editorConfig_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/font.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/font_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/gitignore.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/gradle.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/gradle_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/graphql.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/graphql_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/groovy.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/groovy_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/h.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/h_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/html.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/html_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/http.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/image.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/image_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/java.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/javaScript.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/javaScript_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/java_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/json.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/json_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/jsp.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/jsp_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/markdown.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/markdown_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/maven.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/maven_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/patch.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/patch_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/perl.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/perl_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/powershell.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/powershell_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/properties.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/properties_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/rst.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/rst_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/shell.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/shell_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/sql.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/sql_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/swiftLang.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/swiftLang_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/terraform.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/terraform_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/text.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/text_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/toml.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/toml_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/typeScript.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/typeScript_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/vue.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/wsdl.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/wsdl_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/xhtml.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/xhtml_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/xml.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/xml_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/xsd.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/xsd_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/yaml.svg create mode 100644 linux/assets/file-icons/icons/expui/fileTypes/yaml_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/annotation.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/annotation_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/class.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/class_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/enum.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/enum_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/exception.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/exception_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/folder.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/folder_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/interface.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/interface_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/package.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/package_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/record.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/record_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/resourcesRoot.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/resourcesRoot_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/sourceRoot.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/sourceRoot_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/testResourcesRoot.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/testResourcesRoot_dark.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/testRoot.svg create mode 100644 linux/assets/file-icons/icons/expui/nodes/testRoot_dark.svg delete mode 100644 linux/assets/file-icons/icons/files/adobe.svg delete mode 100644 linux/assets/file-icons/icons/files/agents.svg delete mode 100644 linux/assets/file-icons/icons/files/android.svg delete mode 100644 linux/assets/file-icons/icons/files/angular.svg delete mode 100644 linux/assets/file-icons/icons/files/ansible.svg delete mode 100644 linux/assets/file-icons/icons/files/apple.svg delete mode 100644 linux/assets/file-icons/icons/files/archive.svg delete mode 100644 linux/assets/file-icons/icons/files/arduino.svg delete mode 100644 linux/assets/file-icons/icons/files/astro.svg delete mode 100644 linux/assets/file-icons/icons/files/audio.svg delete mode 100644 linux/assets/file-icons/icons/files/authors.svg delete mode 100644 linux/assets/file-icons/icons/files/babel.svg delete mode 100644 linux/assets/file-icons/icons/files/biome.svg delete mode 100644 linux/assets/file-icons/icons/files/bitbucket.svg delete mode 100644 linux/assets/file-icons/icons/files/blender.svg delete mode 100644 linux/assets/file-icons/icons/files/bun.svg delete mode 100644 linux/assets/file-icons/icons/files/c.svg delete mode 100644 linux/assets/file-icons/icons/files/certificate.svg delete mode 100644 linux/assets/file-icons/icons/files/changelog.svg delete mode 100644 linux/assets/file-icons/icons/files/claude.svg delete mode 100644 linux/assets/file-icons/icons/files/cline.svg delete mode 100644 linux/assets/file-icons/icons/files/clojure.svg delete mode 100644 linux/assets/file-icons/icons/files/cloudflare.svg delete mode 100644 linux/assets/file-icons/icons/files/cmake.svg delete mode 100644 linux/assets/file-icons/icons/files/codex.svg delete mode 100644 linux/assets/file-icons/icons/files/commitlint.svg delete mode 100644 linux/assets/file-icons/icons/files/config.svg delete mode 100644 linux/assets/file-icons/icons/files/copilot.svg delete mode 100644 linux/assets/file-icons/icons/files/cpp.svg delete mode 100644 linux/assets/file-icons/icons/files/csharp.svg delete mode 100644 linux/assets/file-icons/icons/files/cspell.svg delete mode 100644 linux/assets/file-icons/icons/files/css.svg delete mode 100644 linux/assets/file-icons/icons/files/csv.svg delete mode 100644 linux/assets/file-icons/icons/files/cursor.svg delete mode 100644 linux/assets/file-icons/icons/files/cypress.svg delete mode 100644 linux/assets/file-icons/icons/files/dart.svg delete mode 100644 linux/assets/file-icons/icons/files/database.svg delete mode 100644 linux/assets/file-icons/icons/files/deno.svg delete mode 100644 linux/assets/file-icons/icons/files/dependabot.svg delete mode 100644 linux/assets/file-icons/icons/files/devcontainer.svg delete mode 100644 linux/assets/file-icons/icons/files/diff.svg delete mode 100644 linux/assets/file-icons/icons/files/django.svg delete mode 100644 linux/assets/file-icons/icons/files/docker-compose.svg delete mode 100644 linux/assets/file-icons/icons/files/docker.svg delete mode 100644 linux/assets/file-icons/icons/files/document.svg delete mode 100644 linux/assets/file-icons/icons/files/docusaurus.svg delete mode 100644 linux/assets/file-icons/icons/files/drawio.svg delete mode 100644 linux/assets/file-icons/icons/files/drizzle.svg delete mode 100644 linux/assets/file-icons/icons/files/editorconfig.svg delete mode 100644 linux/assets/file-icons/icons/files/electron.svg delete mode 100644 linux/assets/file-icons/icons/files/elixir.svg delete mode 100644 linux/assets/file-icons/icons/files/env.svg delete mode 100644 linux/assets/file-icons/icons/files/erlang.svg delete mode 100644 linux/assets/file-icons/icons/files/error.svg delete mode 100644 linux/assets/file-icons/icons/files/eslint.svg delete mode 100644 linux/assets/file-icons/icons/files/excalidraw.svg delete mode 100644 linux/assets/file-icons/icons/files/fastapi.svg delete mode 100644 linux/assets/file-icons/icons/files/figma.svg delete mode 100644 linux/assets/file-icons/icons/files/file.svg delete mode 100644 linux/assets/file-icons/icons/files/firebase.svg delete mode 100644 linux/assets/file-icons/icons/files/flask.svg delete mode 100644 linux/assets/file-icons/icons/files/font.svg delete mode 100644 linux/assets/file-icons/icons/files/gatsby.svg delete mode 100644 linux/assets/file-icons/icons/files/gemini.svg delete mode 100644 linux/assets/file-icons/icons/files/git.svg delete mode 100644 linux/assets/file-icons/icons/files/github-actions.svg delete mode 100644 linux/assets/file-icons/icons/files/github.svg delete mode 100644 linux/assets/file-icons/icons/files/gitlab.svg delete mode 100644 linux/assets/file-icons/icons/files/go.svg delete mode 100644 linux/assets/file-icons/icons/files/gradle.svg delete mode 100644 linux/assets/file-icons/icons/files/graphql-schema.svg delete mode 100644 linux/assets/file-icons/icons/files/graphql.svg delete mode 100644 linux/assets/file-icons/icons/files/haskell.svg delete mode 100644 linux/assets/file-icons/icons/files/helm.svg delete mode 100644 linux/assets/file-icons/icons/files/html.svg delete mode 100644 linux/assets/file-icons/icons/files/http.svg delete mode 100644 linux/assets/file-icons/icons/files/hurl.svg delete mode 100644 linux/assets/file-icons/icons/files/image.svg delete mode 100644 linux/assets/file-icons/icons/files/index-js.svg delete mode 100644 linux/assets/file-icons/icons/files/index-ts.svg delete mode 100644 linux/assets/file-icons/icons/files/java.svg delete mode 100644 linux/assets/file-icons/icons/files/javascript.svg delete mode 100644 linux/assets/file-icons/icons/files/jenkins.svg delete mode 100644 linux/assets/file-icons/icons/files/jest.svg delete mode 100644 linux/assets/file-icons/icons/files/jsconfig.svg delete mode 100644 linux/assets/file-icons/icons/files/json.svg delete mode 100644 linux/assets/file-icons/icons/files/julia.svg delete mode 100644 linux/assets/file-icons/icons/files/key.svg delete mode 100644 linux/assets/file-icons/icons/files/kotlin.svg delete mode 100644 linux/assets/file-icons/icons/files/kubernetes.svg delete mode 100644 linux/assets/file-icons/icons/files/laravel.svg delete mode 100644 linux/assets/file-icons/icons/files/layout.svg delete mode 100644 linux/assets/file-icons/icons/files/license.svg delete mode 100644 linux/assets/file-icons/icons/files/lintstaged.svg delete mode 100644 linux/assets/file-icons/icons/files/linux.svg delete mode 100644 linux/assets/file-icons/icons/files/lit.svg delete mode 100644 linux/assets/file-icons/icons/files/lithe.svg delete mode 100644 linux/assets/file-icons/icons/files/loading.svg delete mode 100644 linux/assets/file-icons/icons/files/lock.svg delete mode 100644 linux/assets/file-icons/icons/files/log.svg delete mode 100644 linux/assets/file-icons/icons/files/lua.svg delete mode 100644 linux/assets/file-icons/icons/files/makefile.svg delete mode 100644 linux/assets/file-icons/icons/files/markdown.svg delete mode 100644 linux/assets/file-icons/icons/files/markdownlint.svg delete mode 100644 linux/assets/file-icons/icons/files/maven.svg delete mode 100644 linux/assets/file-icons/icons/files/mcp.svg delete mode 100644 linux/assets/file-icons/icons/files/mermaid.svg delete mode 100644 linux/assets/file-icons/icons/files/mongo.svg delete mode 100644 linux/assets/file-icons/icons/files/netlify.svg delete mode 100644 linux/assets/file-icons/icons/files/next.svg delete mode 100644 linux/assets/file-icons/icons/files/nginx.svg delete mode 100644 linux/assets/file-icons/icons/files/nim.svg delete mode 100644 linux/assets/file-icons/icons/files/nix.svg delete mode 100644 linux/assets/file-icons/icons/files/node.svg delete mode 100644 linux/assets/file-icons/icons/files/not-found.svg delete mode 100644 linux/assets/file-icons/icons/files/notebook.svg delete mode 100644 linux/assets/file-icons/icons/files/npm.svg delete mode 100644 linux/assets/file-icons/icons/files/nuxt.svg delete mode 100644 linux/assets/file-icons/icons/files/nx.svg delete mode 100644 linux/assets/file-icons/icons/files/ocaml.svg delete mode 100644 linux/assets/file-icons/icons/files/package.svg delete mode 100644 linux/assets/file-icons/icons/files/page.svg delete mode 100644 linux/assets/file-icons/icons/files/patch.svg delete mode 100644 linux/assets/file-icons/icons/files/pdf.svg delete mode 100644 linux/assets/file-icons/icons/files/perl.svg delete mode 100644 linux/assets/file-icons/icons/files/php.svg delete mode 100644 linux/assets/file-icons/icons/files/playwright.svg delete mode 100644 linux/assets/file-icons/icons/files/pnpm.svg delete mode 100644 linux/assets/file-icons/icons/files/postgres.svg delete mode 100644 linux/assets/file-icons/icons/files/powerpoint.svg delete mode 100644 linux/assets/file-icons/icons/files/prettier.svg delete mode 100644 linux/assets/file-icons/icons/files/prisma.svg delete mode 100644 linux/assets/file-icons/icons/files/proto.svg delete mode 100644 linux/assets/file-icons/icons/files/python.svg delete mode 100644 linux/assets/file-icons/icons/files/qwik.svg delete mode 100644 linux/assets/file-icons/icons/files/r.svg delete mode 100644 linux/assets/file-icons/icons/files/react.svg delete mode 100644 linux/assets/file-icons/icons/files/redis.svg delete mode 100644 linux/assets/file-icons/icons/files/remix.svg delete mode 100644 linux/assets/file-icons/icons/files/renovate.svg delete mode 100644 linux/assets/file-icons/icons/files/rescript.svg delete mode 100644 linux/assets/file-icons/icons/files/rollup.svg delete mode 100644 linux/assets/file-icons/icons/files/route.svg delete mode 100644 linux/assets/file-icons/icons/files/rspack.svg delete mode 100644 linux/assets/file-icons/icons/files/ruby.svg delete mode 100644 linux/assets/file-icons/icons/files/rust.svg delete mode 100644 linux/assets/file-icons/icons/files/sass.svg delete mode 100644 linux/assets/file-icons/icons/files/scala.svg delete mode 100644 linux/assets/file-icons/icons/files/security.svg delete mode 100644 linux/assets/file-icons/icons/files/shell.svg delete mode 100644 linux/assets/file-icons/icons/files/sketch.svg delete mode 100644 linux/assets/file-icons/icons/files/solid.svg delete mode 100644 linux/assets/file-icons/icons/files/solidity.svg delete mode 100644 linux/assets/file-icons/icons/files/spreadsheet.svg delete mode 100644 linux/assets/file-icons/icons/files/sql.svg delete mode 100644 linux/assets/file-icons/icons/files/storybook.svg delete mode 100644 linux/assets/file-icons/icons/files/stylelint.svg delete mode 100644 linux/assets/file-icons/icons/files/supabase.svg delete mode 100644 linux/assets/file-icons/icons/files/svelte.svg delete mode 100644 linux/assets/file-icons/icons/files/svg.svg delete mode 100644 linux/assets/file-icons/icons/files/swc.svg delete mode 100644 linux/assets/file-icons/icons/files/swift.svg delete mode 100644 linux/assets/file-icons/icons/files/tailwind.svg delete mode 100644 linux/assets/file-icons/icons/files/tauri.svg delete mode 100644 linux/assets/file-icons/icons/files/terraform.svg delete mode 100644 linux/assets/file-icons/icons/files/test.svg delete mode 100644 linux/assets/file-icons/icons/files/text.svg delete mode 100644 linux/assets/file-icons/icons/files/toml.svg delete mode 100644 linux/assets/file-icons/icons/files/turborepo.svg delete mode 100644 linux/assets/file-icons/icons/files/typescript.svg delete mode 100644 linux/assets/file-icons/icons/files/vercel-config.svg delete mode 100644 linux/assets/file-icons/icons/files/vercel.svg delete mode 100644 linux/assets/file-icons/icons/files/video.svg delete mode 100644 linux/assets/file-icons/icons/files/vite.svg delete mode 100644 linux/assets/file-icons/icons/files/vitest.svg delete mode 100644 linux/assets/file-icons/icons/files/vue.svg delete mode 100644 linux/assets/file-icons/icons/files/warning.svg delete mode 100644 linux/assets/file-icons/icons/files/wasm.svg delete mode 100644 linux/assets/file-icons/icons/files/webpack.svg delete mode 100644 linux/assets/file-icons/icons/files/windows.svg delete mode 100644 linux/assets/file-icons/icons/files/word.svg delete mode 100644 linux/assets/file-icons/icons/files/xcode.svg delete mode 100644 linux/assets/file-icons/icons/files/xml.svg delete mode 100644 linux/assets/file-icons/icons/files/yaml.svg delete mode 100644 linux/assets/file-icons/icons/files/yarn.svg delete mode 100644 linux/assets/file-icons/icons/files/zig.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-ai-open.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-ai.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-assets-open.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-assets.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-build-open.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-build.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-cloud-open.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-cloud.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-components-open.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-components.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-config-open.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-config.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-database-open.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-database.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-docs-open.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-docs.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-extensions-open.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-extensions.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-git-open.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-git.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-locales-open.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-locales.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-mobile-open.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-mobile.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-open.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-packages-open.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-packages.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-routes-open.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-routes.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-rust-open.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-rust.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-scripts-open.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-scripts.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-security-open.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-security.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-source-open.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-source.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-styles-open.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-styles.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-test-open.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder-test.svg delete mode 100644 linux/assets/file-icons/icons/folders/folder.svg delete mode 100644 linux/assets/file-icons/icons/light/files/adobe.svg delete mode 100644 linux/assets/file-icons/icons/light/files/agents.svg delete mode 100644 linux/assets/file-icons/icons/light/files/android.svg delete mode 100644 linux/assets/file-icons/icons/light/files/angular.svg delete mode 100644 linux/assets/file-icons/icons/light/files/ansible.svg delete mode 100644 linux/assets/file-icons/icons/light/files/apple.svg delete mode 100644 linux/assets/file-icons/icons/light/files/archive.svg delete mode 100644 linux/assets/file-icons/icons/light/files/arduino.svg delete mode 100644 linux/assets/file-icons/icons/light/files/astro.svg delete mode 100644 linux/assets/file-icons/icons/light/files/audio.svg delete mode 100644 linux/assets/file-icons/icons/light/files/authors.svg delete mode 100644 linux/assets/file-icons/icons/light/files/babel.svg delete mode 100644 linux/assets/file-icons/icons/light/files/biome.svg delete mode 100644 linux/assets/file-icons/icons/light/files/bitbucket.svg delete mode 100644 linux/assets/file-icons/icons/light/files/blender.svg delete mode 100644 linux/assets/file-icons/icons/light/files/bun.svg delete mode 100644 linux/assets/file-icons/icons/light/files/c.svg delete mode 100644 linux/assets/file-icons/icons/light/files/certificate.svg delete mode 100644 linux/assets/file-icons/icons/light/files/changelog.svg delete mode 100644 linux/assets/file-icons/icons/light/files/claude.svg delete mode 100644 linux/assets/file-icons/icons/light/files/cline.svg delete mode 100644 linux/assets/file-icons/icons/light/files/clojure.svg delete mode 100644 linux/assets/file-icons/icons/light/files/cloudflare.svg delete mode 100644 linux/assets/file-icons/icons/light/files/cmake.svg delete mode 100644 linux/assets/file-icons/icons/light/files/codex.svg delete mode 100644 linux/assets/file-icons/icons/light/files/commitlint.svg delete mode 100644 linux/assets/file-icons/icons/light/files/config.svg delete mode 100644 linux/assets/file-icons/icons/light/files/copilot.svg delete mode 100644 linux/assets/file-icons/icons/light/files/cpp.svg delete mode 100644 linux/assets/file-icons/icons/light/files/csharp.svg delete mode 100644 linux/assets/file-icons/icons/light/files/cspell.svg delete mode 100644 linux/assets/file-icons/icons/light/files/css.svg delete mode 100644 linux/assets/file-icons/icons/light/files/csv.svg delete mode 100644 linux/assets/file-icons/icons/light/files/cursor.svg delete mode 100644 linux/assets/file-icons/icons/light/files/cypress.svg delete mode 100644 linux/assets/file-icons/icons/light/files/dart.svg delete mode 100644 linux/assets/file-icons/icons/light/files/database.svg delete mode 100644 linux/assets/file-icons/icons/light/files/deno.svg delete mode 100644 linux/assets/file-icons/icons/light/files/dependabot.svg delete mode 100644 linux/assets/file-icons/icons/light/files/devcontainer.svg delete mode 100644 linux/assets/file-icons/icons/light/files/diff.svg delete mode 100644 linux/assets/file-icons/icons/light/files/django.svg delete mode 100644 linux/assets/file-icons/icons/light/files/docker-compose.svg delete mode 100644 linux/assets/file-icons/icons/light/files/docker.svg delete mode 100644 linux/assets/file-icons/icons/light/files/document.svg delete mode 100644 linux/assets/file-icons/icons/light/files/docusaurus.svg delete mode 100644 linux/assets/file-icons/icons/light/files/drawio.svg delete mode 100644 linux/assets/file-icons/icons/light/files/drizzle.svg delete mode 100644 linux/assets/file-icons/icons/light/files/editorconfig.svg delete mode 100644 linux/assets/file-icons/icons/light/files/electron.svg delete mode 100644 linux/assets/file-icons/icons/light/files/elixir.svg delete mode 100644 linux/assets/file-icons/icons/light/files/env.svg delete mode 100644 linux/assets/file-icons/icons/light/files/erlang.svg delete mode 100644 linux/assets/file-icons/icons/light/files/error.svg delete mode 100644 linux/assets/file-icons/icons/light/files/eslint.svg delete mode 100644 linux/assets/file-icons/icons/light/files/excalidraw.svg delete mode 100644 linux/assets/file-icons/icons/light/files/fastapi.svg delete mode 100644 linux/assets/file-icons/icons/light/files/figma.svg delete mode 100644 linux/assets/file-icons/icons/light/files/file.svg delete mode 100644 linux/assets/file-icons/icons/light/files/firebase.svg delete mode 100644 linux/assets/file-icons/icons/light/files/flask.svg delete mode 100644 linux/assets/file-icons/icons/light/files/font.svg delete mode 100644 linux/assets/file-icons/icons/light/files/gatsby.svg delete mode 100644 linux/assets/file-icons/icons/light/files/gemini.svg delete mode 100644 linux/assets/file-icons/icons/light/files/git.svg delete mode 100644 linux/assets/file-icons/icons/light/files/github-actions.svg delete mode 100644 linux/assets/file-icons/icons/light/files/github.svg delete mode 100644 linux/assets/file-icons/icons/light/files/gitlab.svg delete mode 100644 linux/assets/file-icons/icons/light/files/go.svg delete mode 100644 linux/assets/file-icons/icons/light/files/gradle.svg delete mode 100644 linux/assets/file-icons/icons/light/files/graphql-schema.svg delete mode 100644 linux/assets/file-icons/icons/light/files/graphql.svg delete mode 100644 linux/assets/file-icons/icons/light/files/haskell.svg delete mode 100644 linux/assets/file-icons/icons/light/files/helm.svg delete mode 100644 linux/assets/file-icons/icons/light/files/html.svg delete mode 100644 linux/assets/file-icons/icons/light/files/http.svg delete mode 100644 linux/assets/file-icons/icons/light/files/hurl.svg delete mode 100644 linux/assets/file-icons/icons/light/files/image.svg delete mode 100644 linux/assets/file-icons/icons/light/files/index-js.svg delete mode 100644 linux/assets/file-icons/icons/light/files/index-ts.svg delete mode 100644 linux/assets/file-icons/icons/light/files/java.svg delete mode 100644 linux/assets/file-icons/icons/light/files/javascript.svg delete mode 100644 linux/assets/file-icons/icons/light/files/jenkins.svg delete mode 100644 linux/assets/file-icons/icons/light/files/jest.svg delete mode 100644 linux/assets/file-icons/icons/light/files/jsconfig.svg delete mode 100644 linux/assets/file-icons/icons/light/files/json.svg delete mode 100644 linux/assets/file-icons/icons/light/files/julia.svg delete mode 100644 linux/assets/file-icons/icons/light/files/key.svg delete mode 100644 linux/assets/file-icons/icons/light/files/kotlin.svg delete mode 100644 linux/assets/file-icons/icons/light/files/kubernetes.svg delete mode 100644 linux/assets/file-icons/icons/light/files/laravel.svg delete mode 100644 linux/assets/file-icons/icons/light/files/layout.svg delete mode 100644 linux/assets/file-icons/icons/light/files/license.svg delete mode 100644 linux/assets/file-icons/icons/light/files/lintstaged.svg delete mode 100644 linux/assets/file-icons/icons/light/files/linux.svg delete mode 100644 linux/assets/file-icons/icons/light/files/lit.svg delete mode 100644 linux/assets/file-icons/icons/light/files/lithe.svg delete mode 100644 linux/assets/file-icons/icons/light/files/loading.svg delete mode 100644 linux/assets/file-icons/icons/light/files/lock.svg delete mode 100644 linux/assets/file-icons/icons/light/files/log.svg delete mode 100644 linux/assets/file-icons/icons/light/files/lua.svg delete mode 100644 linux/assets/file-icons/icons/light/files/makefile.svg delete mode 100644 linux/assets/file-icons/icons/light/files/markdown.svg delete mode 100644 linux/assets/file-icons/icons/light/files/markdownlint.svg delete mode 100644 linux/assets/file-icons/icons/light/files/maven.svg delete mode 100644 linux/assets/file-icons/icons/light/files/mcp.svg delete mode 100644 linux/assets/file-icons/icons/light/files/mermaid.svg delete mode 100644 linux/assets/file-icons/icons/light/files/mongo.svg delete mode 100644 linux/assets/file-icons/icons/light/files/netlify.svg delete mode 100644 linux/assets/file-icons/icons/light/files/next.svg delete mode 100644 linux/assets/file-icons/icons/light/files/nginx.svg delete mode 100644 linux/assets/file-icons/icons/light/files/nim.svg delete mode 100644 linux/assets/file-icons/icons/light/files/nix.svg delete mode 100644 linux/assets/file-icons/icons/light/files/node.svg delete mode 100644 linux/assets/file-icons/icons/light/files/not-found.svg delete mode 100644 linux/assets/file-icons/icons/light/files/notebook.svg delete mode 100644 linux/assets/file-icons/icons/light/files/npm.svg delete mode 100644 linux/assets/file-icons/icons/light/files/nuxt.svg delete mode 100644 linux/assets/file-icons/icons/light/files/nx.svg delete mode 100644 linux/assets/file-icons/icons/light/files/ocaml.svg delete mode 100644 linux/assets/file-icons/icons/light/files/package.svg delete mode 100644 linux/assets/file-icons/icons/light/files/page.svg delete mode 100644 linux/assets/file-icons/icons/light/files/patch.svg delete mode 100644 linux/assets/file-icons/icons/light/files/pdf.svg delete mode 100644 linux/assets/file-icons/icons/light/files/perl.svg delete mode 100644 linux/assets/file-icons/icons/light/files/php.svg delete mode 100644 linux/assets/file-icons/icons/light/files/playwright.svg delete mode 100644 linux/assets/file-icons/icons/light/files/pnpm.svg delete mode 100644 linux/assets/file-icons/icons/light/files/postgres.svg delete mode 100644 linux/assets/file-icons/icons/light/files/powerpoint.svg delete mode 100644 linux/assets/file-icons/icons/light/files/prettier.svg delete mode 100644 linux/assets/file-icons/icons/light/files/prisma.svg delete mode 100644 linux/assets/file-icons/icons/light/files/proto.svg delete mode 100644 linux/assets/file-icons/icons/light/files/python.svg delete mode 100644 linux/assets/file-icons/icons/light/files/qwik.svg delete mode 100644 linux/assets/file-icons/icons/light/files/r.svg delete mode 100644 linux/assets/file-icons/icons/light/files/react.svg delete mode 100644 linux/assets/file-icons/icons/light/files/redis.svg delete mode 100644 linux/assets/file-icons/icons/light/files/remix.svg delete mode 100644 linux/assets/file-icons/icons/light/files/renovate.svg delete mode 100644 linux/assets/file-icons/icons/light/files/rescript.svg delete mode 100644 linux/assets/file-icons/icons/light/files/rollup.svg delete mode 100644 linux/assets/file-icons/icons/light/files/route.svg delete mode 100644 linux/assets/file-icons/icons/light/files/rspack.svg delete mode 100644 linux/assets/file-icons/icons/light/files/ruby.svg delete mode 100644 linux/assets/file-icons/icons/light/files/rust.svg delete mode 100644 linux/assets/file-icons/icons/light/files/sass.svg delete mode 100644 linux/assets/file-icons/icons/light/files/scala.svg delete mode 100644 linux/assets/file-icons/icons/light/files/security.svg delete mode 100644 linux/assets/file-icons/icons/light/files/shell.svg delete mode 100644 linux/assets/file-icons/icons/light/files/sketch.svg delete mode 100644 linux/assets/file-icons/icons/light/files/solid.svg delete mode 100644 linux/assets/file-icons/icons/light/files/solidity.svg delete mode 100644 linux/assets/file-icons/icons/light/files/spreadsheet.svg delete mode 100644 linux/assets/file-icons/icons/light/files/sql.svg delete mode 100644 linux/assets/file-icons/icons/light/files/storybook.svg delete mode 100644 linux/assets/file-icons/icons/light/files/stylelint.svg delete mode 100644 linux/assets/file-icons/icons/light/files/supabase.svg delete mode 100644 linux/assets/file-icons/icons/light/files/svelte.svg delete mode 100644 linux/assets/file-icons/icons/light/files/svg.svg delete mode 100644 linux/assets/file-icons/icons/light/files/swc.svg delete mode 100644 linux/assets/file-icons/icons/light/files/swift.svg delete mode 100644 linux/assets/file-icons/icons/light/files/tailwind.svg delete mode 100644 linux/assets/file-icons/icons/light/files/tauri.svg delete mode 100644 linux/assets/file-icons/icons/light/files/terraform.svg delete mode 100644 linux/assets/file-icons/icons/light/files/test.svg delete mode 100644 linux/assets/file-icons/icons/light/files/text.svg delete mode 100644 linux/assets/file-icons/icons/light/files/toml.svg delete mode 100644 linux/assets/file-icons/icons/light/files/turborepo.svg delete mode 100644 linux/assets/file-icons/icons/light/files/typescript.svg delete mode 100644 linux/assets/file-icons/icons/light/files/vercel-config.svg delete mode 100644 linux/assets/file-icons/icons/light/files/vercel.svg delete mode 100644 linux/assets/file-icons/icons/light/files/video.svg delete mode 100644 linux/assets/file-icons/icons/light/files/vite.svg delete mode 100644 linux/assets/file-icons/icons/light/files/vitest.svg delete mode 100644 linux/assets/file-icons/icons/light/files/vue.svg delete mode 100644 linux/assets/file-icons/icons/light/files/warning.svg delete mode 100644 linux/assets/file-icons/icons/light/files/wasm.svg delete mode 100644 linux/assets/file-icons/icons/light/files/webpack.svg delete mode 100644 linux/assets/file-icons/icons/light/files/windows.svg delete mode 100644 linux/assets/file-icons/icons/light/files/word.svg delete mode 100644 linux/assets/file-icons/icons/light/files/xcode.svg delete mode 100644 linux/assets/file-icons/icons/light/files/xml.svg delete mode 100644 linux/assets/file-icons/icons/light/files/yaml.svg delete mode 100644 linux/assets/file-icons/icons/light/files/yarn.svg delete mode 100644 linux/assets/file-icons/icons/light/files/zig.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-ai-open.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-ai.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-assets-open.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-assets.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-build-open.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-build.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-cloud-open.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-cloud.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-components-open.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-components.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-config-open.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-config.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-database-open.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-database.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-docs-open.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-docs.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-extensions-open.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-extensions.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-git-open.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-git.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-locales-open.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-locales.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-mobile-open.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-mobile.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-open.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-packages-open.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-packages.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-routes-open.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-routes.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-rust-open.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-rust.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-scripts-open.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-scripts.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-security-open.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-security.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-source-open.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-source.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-styles-open.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-styles.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-test-open.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder-test.svg delete mode 100644 linux/assets/file-icons/icons/light/folders/folder.svg diff --git a/linux/Cargo.toml b/linux/Cargo.toml index dba77ba7f..497950b15 100644 --- a/linux/Cargo.toml +++ b/linux/Cargo.toml @@ -5,7 +5,19 @@ edition = "2021" [dependencies] rfd = "0.16" -gpui-kit = "0.6" +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" serde = { version = "1", features = ["derive"] } 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/icons/files/adobe.svg b/linux/assets/file-icons/icons/files/adobe.svg deleted file mode 100644 index b201a3451..000000000 --- a/linux/assets/file-icons/icons/files/adobe.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - A - diff --git a/linux/assets/file-icons/icons/files/agents.svg b/linux/assets/file-icons/icons/files/agents.svg deleted file mode 100644 index 6d6d2df2f..000000000 --- a/linux/assets/file-icons/icons/files/agents.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - AI - diff --git a/linux/assets/file-icons/icons/files/android.svg b/linux/assets/file-icons/icons/files/android.svg deleted file mode 100644 index 231f6d43d..000000000 --- a/linux/assets/file-icons/icons/files/android.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/angular.svg b/linux/assets/file-icons/icons/files/angular.svg deleted file mode 100644 index 30fec93cc..000000000 --- a/linux/assets/file-icons/icons/files/angular.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/ansible.svg b/linux/assets/file-icons/icons/files/ansible.svg deleted file mode 100644 index fff9e0b79..000000000 --- a/linux/assets/file-icons/icons/files/ansible.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - A - diff --git a/linux/assets/file-icons/icons/files/apple.svg b/linux/assets/file-icons/icons/files/apple.svg deleted file mode 100644 index a34cd9b5c..000000000 --- a/linux/assets/file-icons/icons/files/apple.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/archive.svg b/linux/assets/file-icons/icons/files/archive.svg deleted file mode 100644 index d3822e3c8..000000000 --- a/linux/assets/file-icons/icons/files/archive.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/arduino.svg b/linux/assets/file-icons/icons/files/arduino.svg deleted file mode 100644 index 63b1d77eb..000000000 --- a/linux/assets/file-icons/icons/files/arduino.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - A - diff --git a/linux/assets/file-icons/icons/files/astro.svg b/linux/assets/file-icons/icons/files/astro.svg deleted file mode 100644 index ecb1c91ec..000000000 --- a/linux/assets/file-icons/icons/files/astro.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - A - diff --git a/linux/assets/file-icons/icons/files/audio.svg b/linux/assets/file-icons/icons/files/audio.svg deleted file mode 100644 index 20e064dfe..000000000 --- a/linux/assets/file-icons/icons/files/audio.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/authors.svg b/linux/assets/file-icons/icons/files/authors.svg deleted file mode 100644 index c2e044276..000000000 --- a/linux/assets/file-icons/icons/files/authors.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - BY - diff --git a/linux/assets/file-icons/icons/files/babel.svg b/linux/assets/file-icons/icons/files/babel.svg deleted file mode 100644 index 3d01b0c9c..000000000 --- a/linux/assets/file-icons/icons/files/babel.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - B - diff --git a/linux/assets/file-icons/icons/files/biome.svg b/linux/assets/file-icons/icons/files/biome.svg deleted file mode 100644 index 8a9c038f0..000000000 --- a/linux/assets/file-icons/icons/files/biome.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/bitbucket.svg b/linux/assets/file-icons/icons/files/bitbucket.svg deleted file mode 100644 index b8cda2c49..000000000 --- a/linux/assets/file-icons/icons/files/bitbucket.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/blender.svg b/linux/assets/file-icons/icons/files/blender.svg deleted file mode 100644 index 55a19331a..000000000 --- a/linux/assets/file-icons/icons/files/blender.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - B - diff --git a/linux/assets/file-icons/icons/files/bun.svg b/linux/assets/file-icons/icons/files/bun.svg deleted file mode 100644 index 633a6c56a..000000000 --- a/linux/assets/file-icons/icons/files/bun.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - B - diff --git a/linux/assets/file-icons/icons/files/c.svg b/linux/assets/file-icons/icons/files/c.svg deleted file mode 100644 index 94161033b..000000000 --- a/linux/assets/file-icons/icons/files/c.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - C - diff --git a/linux/assets/file-icons/icons/files/certificate.svg b/linux/assets/file-icons/icons/files/certificate.svg deleted file mode 100644 index 7d7da3ae5..000000000 --- a/linux/assets/file-icons/icons/files/certificate.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - CRT - diff --git a/linux/assets/file-icons/icons/files/changelog.svg b/linux/assets/file-icons/icons/files/changelog.svg deleted file mode 100644 index c9f55d8d9..000000000 --- a/linux/assets/file-icons/icons/files/changelog.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - LOG - diff --git a/linux/assets/file-icons/icons/files/claude.svg b/linux/assets/file-icons/icons/files/claude.svg deleted file mode 100644 index 59fc7a417..000000000 --- a/linux/assets/file-icons/icons/files/claude.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - AI - diff --git a/linux/assets/file-icons/icons/files/cline.svg b/linux/assets/file-icons/icons/files/cline.svg deleted file mode 100644 index ac58f0392..000000000 --- a/linux/assets/file-icons/icons/files/cline.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/clojure.svg b/linux/assets/file-icons/icons/files/clojure.svg deleted file mode 100644 index 506bfd14e..000000000 --- a/linux/assets/file-icons/icons/files/clojure.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/cloudflare.svg b/linux/assets/file-icons/icons/files/cloudflare.svg deleted file mode 100644 index 82a406e40..000000000 --- a/linux/assets/file-icons/icons/files/cloudflare.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - CF - diff --git a/linux/assets/file-icons/icons/files/cmake.svg b/linux/assets/file-icons/icons/files/cmake.svg deleted file mode 100644 index bf7a4b776..000000000 --- a/linux/assets/file-icons/icons/files/cmake.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/codex.svg b/linux/assets/file-icons/icons/files/codex.svg deleted file mode 100644 index 33de1d910..000000000 --- a/linux/assets/file-icons/icons/files/codex.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/commitlint.svg b/linux/assets/file-icons/icons/files/commitlint.svg deleted file mode 100644 index 00b291e99..000000000 --- a/linux/assets/file-icons/icons/files/commitlint.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/config.svg b/linux/assets/file-icons/icons/files/config.svg deleted file mode 100644 index 8a2de5e52..000000000 --- a/linux/assets/file-icons/icons/files/config.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/copilot.svg b/linux/assets/file-icons/icons/files/copilot.svg deleted file mode 100644 index e1aef20fb..000000000 --- a/linux/assets/file-icons/icons/files/copilot.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - AI - diff --git a/linux/assets/file-icons/icons/files/cpp.svg b/linux/assets/file-icons/icons/files/cpp.svg deleted file mode 100644 index ade60b287..000000000 --- a/linux/assets/file-icons/icons/files/cpp.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - C+ - diff --git a/linux/assets/file-icons/icons/files/csharp.svg b/linux/assets/file-icons/icons/files/csharp.svg deleted file mode 100644 index 15d7d14cd..000000000 --- a/linux/assets/file-icons/icons/files/csharp.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - C# - diff --git a/linux/assets/file-icons/icons/files/cspell.svg b/linux/assets/file-icons/icons/files/cspell.svg deleted file mode 100644 index 0c710dac0..000000000 --- a/linux/assets/file-icons/icons/files/cspell.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/css.svg b/linux/assets/file-icons/icons/files/css.svg deleted file mode 100644 index 1bafc3494..000000000 --- a/linux/assets/file-icons/icons/files/css.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - # - diff --git a/linux/assets/file-icons/icons/files/csv.svg b/linux/assets/file-icons/icons/files/csv.svg deleted file mode 100644 index 00ff4d9c8..000000000 --- a/linux/assets/file-icons/icons/files/csv.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - CSV - diff --git a/linux/assets/file-icons/icons/files/cursor.svg b/linux/assets/file-icons/icons/files/cursor.svg deleted file mode 100644 index 0a0383480..000000000 --- a/linux/assets/file-icons/icons/files/cursor.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/cypress.svg b/linux/assets/file-icons/icons/files/cypress.svg deleted file mode 100644 index 3dd11b61d..000000000 --- a/linux/assets/file-icons/icons/files/cypress.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/dart.svg b/linux/assets/file-icons/icons/files/dart.svg deleted file mode 100644 index c326fc377..000000000 --- a/linux/assets/file-icons/icons/files/dart.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - D - diff --git a/linux/assets/file-icons/icons/files/database.svg b/linux/assets/file-icons/icons/files/database.svg deleted file mode 100644 index e313be47d..000000000 --- a/linux/assets/file-icons/icons/files/database.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/deno.svg b/linux/assets/file-icons/icons/files/deno.svg deleted file mode 100644 index d9f5b39d7..000000000 --- a/linux/assets/file-icons/icons/files/deno.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - D - diff --git a/linux/assets/file-icons/icons/files/dependabot.svg b/linux/assets/file-icons/icons/files/dependabot.svg deleted file mode 100644 index 2fac961c6..000000000 --- a/linux/assets/file-icons/icons/files/dependabot.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - D - diff --git a/linux/assets/file-icons/icons/files/devcontainer.svg b/linux/assets/file-icons/icons/files/devcontainer.svg deleted file mode 100644 index d927b1b63..000000000 --- a/linux/assets/file-icons/icons/files/devcontainer.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - DC - diff --git a/linux/assets/file-icons/icons/files/diff.svg b/linux/assets/file-icons/icons/files/diff.svg deleted file mode 100644 index fbbadc24d..000000000 --- a/linux/assets/file-icons/icons/files/diff.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - +- - diff --git a/linux/assets/file-icons/icons/files/django.svg b/linux/assets/file-icons/icons/files/django.svg deleted file mode 100644 index 29e0d32fc..000000000 --- a/linux/assets/file-icons/icons/files/django.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/docker-compose.svg b/linux/assets/file-icons/icons/files/docker-compose.svg deleted file mode 100644 index 68c63db31..000000000 --- a/linux/assets/file-icons/icons/files/docker-compose.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/docker.svg b/linux/assets/file-icons/icons/files/docker.svg deleted file mode 100644 index 9444a3892..000000000 --- a/linux/assets/file-icons/icons/files/docker.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/document.svg b/linux/assets/file-icons/icons/files/document.svg deleted file mode 100644 index 09c9f51bf..000000000 --- a/linux/assets/file-icons/icons/files/document.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - DOC - diff --git a/linux/assets/file-icons/icons/files/docusaurus.svg b/linux/assets/file-icons/icons/files/docusaurus.svg deleted file mode 100644 index 629f18e33..000000000 --- a/linux/assets/file-icons/icons/files/docusaurus.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/drawio.svg b/linux/assets/file-icons/icons/files/drawio.svg deleted file mode 100644 index c337cfccd..000000000 --- a/linux/assets/file-icons/icons/files/drawio.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - D - diff --git a/linux/assets/file-icons/icons/files/drizzle.svg b/linux/assets/file-icons/icons/files/drizzle.svg deleted file mode 100644 index 7041d34a5..000000000 --- a/linux/assets/file-icons/icons/files/drizzle.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/editorconfig.svg b/linux/assets/file-icons/icons/files/editorconfig.svg deleted file mode 100644 index 2250697b1..000000000 --- a/linux/assets/file-icons/icons/files/editorconfig.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/electron.svg b/linux/assets/file-icons/icons/files/electron.svg deleted file mode 100644 index 2bfb7ff1d..000000000 --- a/linux/assets/file-icons/icons/files/electron.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - E - diff --git a/linux/assets/file-icons/icons/files/elixir.svg b/linux/assets/file-icons/icons/files/elixir.svg deleted file mode 100644 index d331e75f3..000000000 --- a/linux/assets/file-icons/icons/files/elixir.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - EX - diff --git a/linux/assets/file-icons/icons/files/env.svg b/linux/assets/file-icons/icons/files/env.svg deleted file mode 100644 index 305241630..000000000 --- a/linux/assets/file-icons/icons/files/env.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/erlang.svg b/linux/assets/file-icons/icons/files/erlang.svg deleted file mode 100644 index 374a8b1e5..000000000 --- a/linux/assets/file-icons/icons/files/erlang.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - ER - diff --git a/linux/assets/file-icons/icons/files/error.svg b/linux/assets/file-icons/icons/files/error.svg deleted file mode 100644 index 6d1526ae9..000000000 --- a/linux/assets/file-icons/icons/files/error.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/eslint.svg b/linux/assets/file-icons/icons/files/eslint.svg deleted file mode 100644 index 2031a67f4..000000000 --- a/linux/assets/file-icons/icons/files/eslint.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - E - diff --git a/linux/assets/file-icons/icons/files/excalidraw.svg b/linux/assets/file-icons/icons/files/excalidraw.svg deleted file mode 100644 index f6c0eeed0..000000000 --- a/linux/assets/file-icons/icons/files/excalidraw.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/fastapi.svg b/linux/assets/file-icons/icons/files/fastapi.svg deleted file mode 100644 index 695f0b01e..000000000 --- a/linux/assets/file-icons/icons/files/fastapi.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - FA - diff --git a/linux/assets/file-icons/icons/files/figma.svg b/linux/assets/file-icons/icons/files/figma.svg deleted file mode 100644 index dbef799af..000000000 --- a/linux/assets/file-icons/icons/files/figma.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - F - diff --git a/linux/assets/file-icons/icons/files/file.svg b/linux/assets/file-icons/icons/files/file.svg deleted file mode 100644 index 59017652f..000000000 --- a/linux/assets/file-icons/icons/files/file.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/firebase.svg b/linux/assets/file-icons/icons/files/firebase.svg deleted file mode 100644 index c118fb9bf..000000000 --- a/linux/assets/file-icons/icons/files/firebase.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/flask.svg b/linux/assets/file-icons/icons/files/flask.svg deleted file mode 100644 index 28bdc7784..000000000 --- a/linux/assets/file-icons/icons/files/flask.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/font.svg b/linux/assets/file-icons/icons/files/font.svg deleted file mode 100644 index 1cc2912bf..000000000 --- a/linux/assets/file-icons/icons/files/font.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/gatsby.svg b/linux/assets/file-icons/icons/files/gatsby.svg deleted file mode 100644 index 7a1302efe..000000000 --- a/linux/assets/file-icons/icons/files/gatsby.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - G - diff --git a/linux/assets/file-icons/icons/files/gemini.svg b/linux/assets/file-icons/icons/files/gemini.svg deleted file mode 100644 index 59aea00b6..000000000 --- a/linux/assets/file-icons/icons/files/gemini.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - G - diff --git a/linux/assets/file-icons/icons/files/git.svg b/linux/assets/file-icons/icons/files/git.svg deleted file mode 100644 index e5cd60b77..000000000 --- a/linux/assets/file-icons/icons/files/git.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/github-actions.svg b/linux/assets/file-icons/icons/files/github-actions.svg deleted file mode 100644 index aab3f321d..000000000 --- a/linux/assets/file-icons/icons/files/github-actions.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - GH - diff --git a/linux/assets/file-icons/icons/files/github.svg b/linux/assets/file-icons/icons/files/github.svg deleted file mode 100644 index 8a1f67d7b..000000000 --- a/linux/assets/file-icons/icons/files/github.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/gitlab.svg b/linux/assets/file-icons/icons/files/gitlab.svg deleted file mode 100644 index 2500a36ae..000000000 --- a/linux/assets/file-icons/icons/files/gitlab.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/go.svg b/linux/assets/file-icons/icons/files/go.svg deleted file mode 100644 index 0174b5232..000000000 --- a/linux/assets/file-icons/icons/files/go.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/gradle.svg b/linux/assets/file-icons/icons/files/gradle.svg deleted file mode 100644 index 267a55ee3..000000000 --- a/linux/assets/file-icons/icons/files/gradle.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - G - diff --git a/linux/assets/file-icons/icons/files/graphql-schema.svg b/linux/assets/file-icons/icons/files/graphql-schema.svg deleted file mode 100644 index 64f8c0f84..000000000 --- a/linux/assets/file-icons/icons/files/graphql-schema.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - GS - diff --git a/linux/assets/file-icons/icons/files/graphql.svg b/linux/assets/file-icons/icons/files/graphql.svg deleted file mode 100644 index bab772115..000000000 --- a/linux/assets/file-icons/icons/files/graphql.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - G - diff --git a/linux/assets/file-icons/icons/files/haskell.svg b/linux/assets/file-icons/icons/files/haskell.svg deleted file mode 100644 index 52ddcaeee..000000000 --- a/linux/assets/file-icons/icons/files/haskell.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - HS - diff --git a/linux/assets/file-icons/icons/files/helm.svg b/linux/assets/file-icons/icons/files/helm.svg deleted file mode 100644 index 5be27ed28..000000000 --- a/linux/assets/file-icons/icons/files/helm.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - H - diff --git a/linux/assets/file-icons/icons/files/html.svg b/linux/assets/file-icons/icons/files/html.svg deleted file mode 100644 index c80ca7a79..000000000 --- a/linux/assets/file-icons/icons/files/html.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - H - diff --git a/linux/assets/file-icons/icons/files/http.svg b/linux/assets/file-icons/icons/files/http.svg deleted file mode 100644 index 5280b368d..000000000 --- a/linux/assets/file-icons/icons/files/http.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - HT - diff --git a/linux/assets/file-icons/icons/files/hurl.svg b/linux/assets/file-icons/icons/files/hurl.svg deleted file mode 100644 index bf70415fa..000000000 --- a/linux/assets/file-icons/icons/files/hurl.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - HU - diff --git a/linux/assets/file-icons/icons/files/image.svg b/linux/assets/file-icons/icons/files/image.svg deleted file mode 100644 index 1d2bb2639..000000000 --- a/linux/assets/file-icons/icons/files/image.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/index-js.svg b/linux/assets/file-icons/icons/files/index-js.svg deleted file mode 100644 index ec34e3867..000000000 --- a/linux/assets/file-icons/icons/files/index-js.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - IDX - diff --git a/linux/assets/file-icons/icons/files/index-ts.svg b/linux/assets/file-icons/icons/files/index-ts.svg deleted file mode 100644 index 5e532a7ed..000000000 --- a/linux/assets/file-icons/icons/files/index-ts.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - IDX - diff --git a/linux/assets/file-icons/icons/files/java.svg b/linux/assets/file-icons/icons/files/java.svg deleted file mode 100644 index 2bc93d42d..000000000 --- a/linux/assets/file-icons/icons/files/java.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/javascript.svg b/linux/assets/file-icons/icons/files/javascript.svg deleted file mode 100644 index 59e3fe6ec..000000000 --- a/linux/assets/file-icons/icons/files/javascript.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - JS - diff --git a/linux/assets/file-icons/icons/files/jenkins.svg b/linux/assets/file-icons/icons/files/jenkins.svg deleted file mode 100644 index 57d712ab0..000000000 --- a/linux/assets/file-icons/icons/files/jenkins.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/jest.svg b/linux/assets/file-icons/icons/files/jest.svg deleted file mode 100644 index 473457b11..000000000 --- a/linux/assets/file-icons/icons/files/jest.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/jsconfig.svg b/linux/assets/file-icons/icons/files/jsconfig.svg deleted file mode 100644 index ce9fe1edd..000000000 --- a/linux/assets/file-icons/icons/files/jsconfig.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/json.svg b/linux/assets/file-icons/icons/files/json.svg deleted file mode 100644 index 943bb57f6..000000000 --- a/linux/assets/file-icons/icons/files/json.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - {} - diff --git a/linux/assets/file-icons/icons/files/julia.svg b/linux/assets/file-icons/icons/files/julia.svg deleted file mode 100644 index 5a2b777b7..000000000 --- a/linux/assets/file-icons/icons/files/julia.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - JL - diff --git a/linux/assets/file-icons/icons/files/key.svg b/linux/assets/file-icons/icons/files/key.svg deleted file mode 100644 index 9348cc4ce..000000000 --- a/linux/assets/file-icons/icons/files/key.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/kotlin.svg b/linux/assets/file-icons/icons/files/kotlin.svg deleted file mode 100644 index 353a11ce8..000000000 --- a/linux/assets/file-icons/icons/files/kotlin.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - K - diff --git a/linux/assets/file-icons/icons/files/kubernetes.svg b/linux/assets/file-icons/icons/files/kubernetes.svg deleted file mode 100644 index 9b38b313b..000000000 --- a/linux/assets/file-icons/icons/files/kubernetes.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - K8 - diff --git a/linux/assets/file-icons/icons/files/laravel.svg b/linux/assets/file-icons/icons/files/laravel.svg deleted file mode 100644 index 7be1f3df4..000000000 --- a/linux/assets/file-icons/icons/files/laravel.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/layout.svg b/linux/assets/file-icons/icons/files/layout.svg deleted file mode 100644 index ccdd1d73f..000000000 --- a/linux/assets/file-icons/icons/files/layout.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - L - diff --git a/linux/assets/file-icons/icons/files/license.svg b/linux/assets/file-icons/icons/files/license.svg deleted file mode 100644 index 053cf8990..000000000 --- a/linux/assets/file-icons/icons/files/license.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - LIC - diff --git a/linux/assets/file-icons/icons/files/lintstaged.svg b/linux/assets/file-icons/icons/files/lintstaged.svg deleted file mode 100644 index dcbb7fbce..000000000 --- a/linux/assets/file-icons/icons/files/lintstaged.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - LS - diff --git a/linux/assets/file-icons/icons/files/linux.svg b/linux/assets/file-icons/icons/files/linux.svg deleted file mode 100644 index b7baaefb1..000000000 --- a/linux/assets/file-icons/icons/files/linux.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/lit.svg b/linux/assets/file-icons/icons/files/lit.svg deleted file mode 100644 index 41956bae5..000000000 --- a/linux/assets/file-icons/icons/files/lit.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/lithe.svg b/linux/assets/file-icons/icons/files/lithe.svg deleted file mode 100644 index 9dd935c7e..000000000 --- a/linux/assets/file-icons/icons/files/lithe.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - L - diff --git a/linux/assets/file-icons/icons/files/loading.svg b/linux/assets/file-icons/icons/files/loading.svg deleted file mode 100644 index bc3f15ad1..000000000 --- a/linux/assets/file-icons/icons/files/loading.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - ... - diff --git a/linux/assets/file-icons/icons/files/lock.svg b/linux/assets/file-icons/icons/files/lock.svg deleted file mode 100644 index 278b20f1c..000000000 --- a/linux/assets/file-icons/icons/files/lock.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/log.svg b/linux/assets/file-icons/icons/files/log.svg deleted file mode 100644 index 936d5e2a3..000000000 --- a/linux/assets/file-icons/icons/files/log.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - LOG - diff --git a/linux/assets/file-icons/icons/files/lua.svg b/linux/assets/file-icons/icons/files/lua.svg deleted file mode 100644 index 25ea107b8..000000000 --- a/linux/assets/file-icons/icons/files/lua.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - L - diff --git a/linux/assets/file-icons/icons/files/makefile.svg b/linux/assets/file-icons/icons/files/makefile.svg deleted file mode 100644 index a927f9c41..000000000 --- a/linux/assets/file-icons/icons/files/makefile.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/markdown.svg b/linux/assets/file-icons/icons/files/markdown.svg deleted file mode 100644 index 027d5207f..000000000 --- a/linux/assets/file-icons/icons/files/markdown.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/markdownlint.svg b/linux/assets/file-icons/icons/files/markdownlint.svg deleted file mode 100644 index 22e601114..000000000 --- a/linux/assets/file-icons/icons/files/markdownlint.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/maven.svg b/linux/assets/file-icons/icons/files/maven.svg deleted file mode 100644 index dd935da72..000000000 --- a/linux/assets/file-icons/icons/files/maven.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - M - diff --git a/linux/assets/file-icons/icons/files/mcp.svg b/linux/assets/file-icons/icons/files/mcp.svg deleted file mode 100644 index 963f893fe..000000000 --- a/linux/assets/file-icons/icons/files/mcp.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - M - diff --git a/linux/assets/file-icons/icons/files/mermaid.svg b/linux/assets/file-icons/icons/files/mermaid.svg deleted file mode 100644 index eb7cab28a..000000000 --- a/linux/assets/file-icons/icons/files/mermaid.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - MM - diff --git a/linux/assets/file-icons/icons/files/mongo.svg b/linux/assets/file-icons/icons/files/mongo.svg deleted file mode 100644 index 96e2a647b..000000000 --- a/linux/assets/file-icons/icons/files/mongo.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/netlify.svg b/linux/assets/file-icons/icons/files/netlify.svg deleted file mode 100644 index 133725c92..000000000 --- a/linux/assets/file-icons/icons/files/netlify.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - N - diff --git a/linux/assets/file-icons/icons/files/next.svg b/linux/assets/file-icons/icons/files/next.svg deleted file mode 100644 index e2d90d83d..000000000 --- a/linux/assets/file-icons/icons/files/next.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - N - diff --git a/linux/assets/file-icons/icons/files/nginx.svg b/linux/assets/file-icons/icons/files/nginx.svg deleted file mode 100644 index 7f1d606bf..000000000 --- a/linux/assets/file-icons/icons/files/nginx.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - N - diff --git a/linux/assets/file-icons/icons/files/nim.svg b/linux/assets/file-icons/icons/files/nim.svg deleted file mode 100644 index ac80cf1a1..000000000 --- a/linux/assets/file-icons/icons/files/nim.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - N - diff --git a/linux/assets/file-icons/icons/files/nix.svg b/linux/assets/file-icons/icons/files/nix.svg deleted file mode 100644 index f8027026f..000000000 --- a/linux/assets/file-icons/icons/files/nix.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - N - diff --git a/linux/assets/file-icons/icons/files/node.svg b/linux/assets/file-icons/icons/files/node.svg deleted file mode 100644 index aaf878edc..000000000 --- a/linux/assets/file-icons/icons/files/node.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - N - diff --git a/linux/assets/file-icons/icons/files/not-found.svg b/linux/assets/file-icons/icons/files/not-found.svg deleted file mode 100644 index b0ef19fe1..000000000 --- a/linux/assets/file-icons/icons/files/not-found.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/notebook.svg b/linux/assets/file-icons/icons/files/notebook.svg deleted file mode 100644 index 3c841c9e8..000000000 --- a/linux/assets/file-icons/icons/files/notebook.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/npm.svg b/linux/assets/file-icons/icons/files/npm.svg deleted file mode 100644 index 26c322283..000000000 --- a/linux/assets/file-icons/icons/files/npm.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - N - diff --git a/linux/assets/file-icons/icons/files/nuxt.svg b/linux/assets/file-icons/icons/files/nuxt.svg deleted file mode 100644 index fb699c2a2..000000000 --- a/linux/assets/file-icons/icons/files/nuxt.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - N - diff --git a/linux/assets/file-icons/icons/files/nx.svg b/linux/assets/file-icons/icons/files/nx.svg deleted file mode 100644 index ef0c748d8..000000000 --- a/linux/assets/file-icons/icons/files/nx.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - NX - diff --git a/linux/assets/file-icons/icons/files/ocaml.svg b/linux/assets/file-icons/icons/files/ocaml.svg deleted file mode 100644 index 02d552768..000000000 --- a/linux/assets/file-icons/icons/files/ocaml.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - ML - diff --git a/linux/assets/file-icons/icons/files/package.svg b/linux/assets/file-icons/icons/files/package.svg deleted file mode 100644 index 51f25f450..000000000 --- a/linux/assets/file-icons/icons/files/package.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/page.svg b/linux/assets/file-icons/icons/files/page.svg deleted file mode 100644 index 860d4e6b4..000000000 --- a/linux/assets/file-icons/icons/files/page.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - P - diff --git a/linux/assets/file-icons/icons/files/patch.svg b/linux/assets/file-icons/icons/files/patch.svg deleted file mode 100644 index ddaa0c327..000000000 --- a/linux/assets/file-icons/icons/files/patch.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - +- - diff --git a/linux/assets/file-icons/icons/files/pdf.svg b/linux/assets/file-icons/icons/files/pdf.svg deleted file mode 100644 index 15236fe62..000000000 --- a/linux/assets/file-icons/icons/files/pdf.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - PDF - diff --git a/linux/assets/file-icons/icons/files/perl.svg b/linux/assets/file-icons/icons/files/perl.svg deleted file mode 100644 index 6e5a983ae..000000000 --- a/linux/assets/file-icons/icons/files/perl.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - PL - diff --git a/linux/assets/file-icons/icons/files/php.svg b/linux/assets/file-icons/icons/files/php.svg deleted file mode 100644 index a40c8ff3c..000000000 --- a/linux/assets/file-icons/icons/files/php.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - P - diff --git a/linux/assets/file-icons/icons/files/playwright.svg b/linux/assets/file-icons/icons/files/playwright.svg deleted file mode 100644 index 68522e996..000000000 --- a/linux/assets/file-icons/icons/files/playwright.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/pnpm.svg b/linux/assets/file-icons/icons/files/pnpm.svg deleted file mode 100644 index fcc30d19e..000000000 --- a/linux/assets/file-icons/icons/files/pnpm.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - P - diff --git a/linux/assets/file-icons/icons/files/postgres.svg b/linux/assets/file-icons/icons/files/postgres.svg deleted file mode 100644 index 0f5a3a35f..000000000 --- a/linux/assets/file-icons/icons/files/postgres.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/powerpoint.svg b/linux/assets/file-icons/icons/files/powerpoint.svg deleted file mode 100644 index 28ff619e1..000000000 --- a/linux/assets/file-icons/icons/files/powerpoint.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - PPT - diff --git a/linux/assets/file-icons/icons/files/prettier.svg b/linux/assets/file-icons/icons/files/prettier.svg deleted file mode 100644 index fc6d6e16a..000000000 --- a/linux/assets/file-icons/icons/files/prettier.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/prisma.svg b/linux/assets/file-icons/icons/files/prisma.svg deleted file mode 100644 index 2d6f445e2..000000000 --- a/linux/assets/file-icons/icons/files/prisma.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/proto.svg b/linux/assets/file-icons/icons/files/proto.svg deleted file mode 100644 index e116a7fdd..000000000 --- a/linux/assets/file-icons/icons/files/proto.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - P - diff --git a/linux/assets/file-icons/icons/files/python.svg b/linux/assets/file-icons/icons/files/python.svg deleted file mode 100644 index 2a3ffb94c..000000000 --- a/linux/assets/file-icons/icons/files/python.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/qwik.svg b/linux/assets/file-icons/icons/files/qwik.svg deleted file mode 100644 index fa7b9a5c9..000000000 --- a/linux/assets/file-icons/icons/files/qwik.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - Q - diff --git a/linux/assets/file-icons/icons/files/r.svg b/linux/assets/file-icons/icons/files/r.svg deleted file mode 100644 index abf4b93c1..000000000 --- a/linux/assets/file-icons/icons/files/r.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - R - diff --git a/linux/assets/file-icons/icons/files/react.svg b/linux/assets/file-icons/icons/files/react.svg deleted file mode 100644 index 88525c8e3..000000000 --- a/linux/assets/file-icons/icons/files/react.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/redis.svg b/linux/assets/file-icons/icons/files/redis.svg deleted file mode 100644 index ef6c2033a..000000000 --- a/linux/assets/file-icons/icons/files/redis.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/remix.svg b/linux/assets/file-icons/icons/files/remix.svg deleted file mode 100644 index e8dbe13ce..000000000 --- a/linux/assets/file-icons/icons/files/remix.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - R - diff --git a/linux/assets/file-icons/icons/files/renovate.svg b/linux/assets/file-icons/icons/files/renovate.svg deleted file mode 100644 index 4690deb44..000000000 --- a/linux/assets/file-icons/icons/files/renovate.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/rescript.svg b/linux/assets/file-icons/icons/files/rescript.svg deleted file mode 100644 index f2d3b7c61..000000000 --- a/linux/assets/file-icons/icons/files/rescript.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - RE - diff --git a/linux/assets/file-icons/icons/files/rollup.svg b/linux/assets/file-icons/icons/files/rollup.svg deleted file mode 100644 index 869e62dde..000000000 --- a/linux/assets/file-icons/icons/files/rollup.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - R - diff --git a/linux/assets/file-icons/icons/files/route.svg b/linux/assets/file-icons/icons/files/route.svg deleted file mode 100644 index 5e2a4c710..000000000 --- a/linux/assets/file-icons/icons/files/route.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - RT - diff --git a/linux/assets/file-icons/icons/files/rspack.svg b/linux/assets/file-icons/icons/files/rspack.svg deleted file mode 100644 index 302fab094..000000000 --- a/linux/assets/file-icons/icons/files/rspack.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - R - diff --git a/linux/assets/file-icons/icons/files/ruby.svg b/linux/assets/file-icons/icons/files/ruby.svg deleted file mode 100644 index 818e46b2f..000000000 --- a/linux/assets/file-icons/icons/files/ruby.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - RB - diff --git a/linux/assets/file-icons/icons/files/rust.svg b/linux/assets/file-icons/icons/files/rust.svg deleted file mode 100644 index 3b109303d..000000000 --- a/linux/assets/file-icons/icons/files/rust.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/sass.svg b/linux/assets/file-icons/icons/files/sass.svg deleted file mode 100644 index 93d0d5417..000000000 --- a/linux/assets/file-icons/icons/files/sass.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - S - diff --git a/linux/assets/file-icons/icons/files/scala.svg b/linux/assets/file-icons/icons/files/scala.svg deleted file mode 100644 index 2899b8ef7..000000000 --- a/linux/assets/file-icons/icons/files/scala.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - S - diff --git a/linux/assets/file-icons/icons/files/security.svg b/linux/assets/file-icons/icons/files/security.svg deleted file mode 100644 index e8cfb3b5d..000000000 --- a/linux/assets/file-icons/icons/files/security.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - SEC - diff --git a/linux/assets/file-icons/icons/files/shell.svg b/linux/assets/file-icons/icons/files/shell.svg deleted file mode 100644 index 95aecf385..000000000 --- a/linux/assets/file-icons/icons/files/shell.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/sketch.svg b/linux/assets/file-icons/icons/files/sketch.svg deleted file mode 100644 index 1fcb9ba0b..000000000 --- a/linux/assets/file-icons/icons/files/sketch.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - S - diff --git a/linux/assets/file-icons/icons/files/solid.svg b/linux/assets/file-icons/icons/files/solid.svg deleted file mode 100644 index 1eb34aeef..000000000 --- a/linux/assets/file-icons/icons/files/solid.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - S - diff --git a/linux/assets/file-icons/icons/files/solidity.svg b/linux/assets/file-icons/icons/files/solidity.svg deleted file mode 100644 index 1a9a884d8..000000000 --- a/linux/assets/file-icons/icons/files/solidity.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - S - diff --git a/linux/assets/file-icons/icons/files/spreadsheet.svg b/linux/assets/file-icons/icons/files/spreadsheet.svg deleted file mode 100644 index 591053b33..000000000 --- a/linux/assets/file-icons/icons/files/spreadsheet.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - XLS - diff --git a/linux/assets/file-icons/icons/files/sql.svg b/linux/assets/file-icons/icons/files/sql.svg deleted file mode 100644 index 886f5f0e8..000000000 --- a/linux/assets/file-icons/icons/files/sql.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/storybook.svg b/linux/assets/file-icons/icons/files/storybook.svg deleted file mode 100644 index 508b0d4b5..000000000 --- a/linux/assets/file-icons/icons/files/storybook.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/stylelint.svg b/linux/assets/file-icons/icons/files/stylelint.svg deleted file mode 100644 index ddffe630a..000000000 --- a/linux/assets/file-icons/icons/files/stylelint.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - SL - diff --git a/linux/assets/file-icons/icons/files/supabase.svg b/linux/assets/file-icons/icons/files/supabase.svg deleted file mode 100644 index 7fa1d07fb..000000000 --- a/linux/assets/file-icons/icons/files/supabase.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/svelte.svg b/linux/assets/file-icons/icons/files/svelte.svg deleted file mode 100644 index fd08f3199..000000000 --- a/linux/assets/file-icons/icons/files/svelte.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - S - diff --git a/linux/assets/file-icons/icons/files/svg.svg b/linux/assets/file-icons/icons/files/svg.svg deleted file mode 100644 index 82fe92ab9..000000000 --- a/linux/assets/file-icons/icons/files/svg.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - S - diff --git a/linux/assets/file-icons/icons/files/swc.svg b/linux/assets/file-icons/icons/files/swc.svg deleted file mode 100644 index 1a45cc869..000000000 --- a/linux/assets/file-icons/icons/files/swc.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - S - diff --git a/linux/assets/file-icons/icons/files/swift.svg b/linux/assets/file-icons/icons/files/swift.svg deleted file mode 100644 index 0821c2986..000000000 --- a/linux/assets/file-icons/icons/files/swift.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/tailwind.svg b/linux/assets/file-icons/icons/files/tailwind.svg deleted file mode 100644 index 6142d3e56..000000000 --- a/linux/assets/file-icons/icons/files/tailwind.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - T - diff --git a/linux/assets/file-icons/icons/files/tauri.svg b/linux/assets/file-icons/icons/files/tauri.svg deleted file mode 100644 index d98590ba6..000000000 --- a/linux/assets/file-icons/icons/files/tauri.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/terraform.svg b/linux/assets/file-icons/icons/files/terraform.svg deleted file mode 100644 index f02174afb..000000000 --- a/linux/assets/file-icons/icons/files/terraform.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - TF - diff --git a/linux/assets/file-icons/icons/files/test.svg b/linux/assets/file-icons/icons/files/test.svg deleted file mode 100644 index 640d48ccb..000000000 --- a/linux/assets/file-icons/icons/files/test.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/text.svg b/linux/assets/file-icons/icons/files/text.svg deleted file mode 100644 index ce1557bcb..000000000 --- a/linux/assets/file-icons/icons/files/text.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - TXT - diff --git a/linux/assets/file-icons/icons/files/toml.svg b/linux/assets/file-icons/icons/files/toml.svg deleted file mode 100644 index 690cf1e3b..000000000 --- a/linux/assets/file-icons/icons/files/toml.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/turborepo.svg b/linux/assets/file-icons/icons/files/turborepo.svg deleted file mode 100644 index 01c8c146d..000000000 --- a/linux/assets/file-icons/icons/files/turborepo.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - T - diff --git a/linux/assets/file-icons/icons/files/typescript.svg b/linux/assets/file-icons/icons/files/typescript.svg deleted file mode 100644 index 6bb44cce8..000000000 --- a/linux/assets/file-icons/icons/files/typescript.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - TS - diff --git a/linux/assets/file-icons/icons/files/vercel-config.svg b/linux/assets/file-icons/icons/files/vercel-config.svg deleted file mode 100644 index 520eb3b64..000000000 --- a/linux/assets/file-icons/icons/files/vercel-config.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - VC - diff --git a/linux/assets/file-icons/icons/files/vercel.svg b/linux/assets/file-icons/icons/files/vercel.svg deleted file mode 100644 index 045058ceb..000000000 --- a/linux/assets/file-icons/icons/files/vercel.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - V - diff --git a/linux/assets/file-icons/icons/files/video.svg b/linux/assets/file-icons/icons/files/video.svg deleted file mode 100644 index dad213208..000000000 --- a/linux/assets/file-icons/icons/files/video.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/vite.svg b/linux/assets/file-icons/icons/files/vite.svg deleted file mode 100644 index 7101b8e53..000000000 --- a/linux/assets/file-icons/icons/files/vite.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - V - diff --git a/linux/assets/file-icons/icons/files/vitest.svg b/linux/assets/file-icons/icons/files/vitest.svg deleted file mode 100644 index 3a4824a93..000000000 --- a/linux/assets/file-icons/icons/files/vitest.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/vue.svg b/linux/assets/file-icons/icons/files/vue.svg deleted file mode 100644 index afddcd1dd..000000000 --- a/linux/assets/file-icons/icons/files/vue.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - V - diff --git a/linux/assets/file-icons/icons/files/warning.svg b/linux/assets/file-icons/icons/files/warning.svg deleted file mode 100644 index 576f04b1d..000000000 --- a/linux/assets/file-icons/icons/files/warning.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/wasm.svg b/linux/assets/file-icons/icons/files/wasm.svg deleted file mode 100644 index ed16980f5..000000000 --- a/linux/assets/file-icons/icons/files/wasm.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - W - diff --git a/linux/assets/file-icons/icons/files/webpack.svg b/linux/assets/file-icons/icons/files/webpack.svg deleted file mode 100644 index 512e1c057..000000000 --- a/linux/assets/file-icons/icons/files/webpack.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - W - diff --git a/linux/assets/file-icons/icons/files/windows.svg b/linux/assets/file-icons/icons/files/windows.svg deleted file mode 100644 index dccc1f9bf..000000000 --- a/linux/assets/file-icons/icons/files/windows.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - W - diff --git a/linux/assets/file-icons/icons/files/word.svg b/linux/assets/file-icons/icons/files/word.svg deleted file mode 100644 index 850083eff..000000000 --- a/linux/assets/file-icons/icons/files/word.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - DOC - diff --git a/linux/assets/file-icons/icons/files/xcode.svg b/linux/assets/file-icons/icons/files/xcode.svg deleted file mode 100644 index 9b63fe1cd..000000000 --- a/linux/assets/file-icons/icons/files/xcode.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/files/xml.svg b/linux/assets/file-icons/icons/files/xml.svg deleted file mode 100644 index 7905a6bdf..000000000 --- a/linux/assets/file-icons/icons/files/xml.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - <> - diff --git a/linux/assets/file-icons/icons/files/yaml.svg b/linux/assets/file-icons/icons/files/yaml.svg deleted file mode 100644 index 262b00903..000000000 --- a/linux/assets/file-icons/icons/files/yaml.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - Y - diff --git a/linux/assets/file-icons/icons/files/yarn.svg b/linux/assets/file-icons/icons/files/yarn.svg deleted file mode 100644 index 9ee784fc6..000000000 --- a/linux/assets/file-icons/icons/files/yarn.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - Y - diff --git a/linux/assets/file-icons/icons/files/zig.svg b/linux/assets/file-icons/icons/files/zig.svg deleted file mode 100644 index 613c135a4..000000000 --- a/linux/assets/file-icons/icons/files/zig.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-ai-open.svg b/linux/assets/file-icons/icons/folders/folder-ai-open.svg deleted file mode 100644 index c002af7f1..000000000 --- a/linux/assets/file-icons/icons/folders/folder-ai-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-ai.svg b/linux/assets/file-icons/icons/folders/folder-ai.svg deleted file mode 100644 index c2a7d27f1..000000000 --- a/linux/assets/file-icons/icons/folders/folder-ai.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-assets-open.svg b/linux/assets/file-icons/icons/folders/folder-assets-open.svg deleted file mode 100644 index 07c3cc1c8..000000000 --- a/linux/assets/file-icons/icons/folders/folder-assets-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-assets.svg b/linux/assets/file-icons/icons/folders/folder-assets.svg deleted file mode 100644 index a4505fc3f..000000000 --- a/linux/assets/file-icons/icons/folders/folder-assets.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-build-open.svg b/linux/assets/file-icons/icons/folders/folder-build-open.svg deleted file mode 100644 index 677645f72..000000000 --- a/linux/assets/file-icons/icons/folders/folder-build-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-build.svg b/linux/assets/file-icons/icons/folders/folder-build.svg deleted file mode 100644 index f8ae479c8..000000000 --- a/linux/assets/file-icons/icons/folders/folder-build.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-cloud-open.svg b/linux/assets/file-icons/icons/folders/folder-cloud-open.svg deleted file mode 100644 index 93e712555..000000000 --- a/linux/assets/file-icons/icons/folders/folder-cloud-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-cloud.svg b/linux/assets/file-icons/icons/folders/folder-cloud.svg deleted file mode 100644 index fd76ff9e7..000000000 --- a/linux/assets/file-icons/icons/folders/folder-cloud.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-components-open.svg b/linux/assets/file-icons/icons/folders/folder-components-open.svg deleted file mode 100644 index eaf07554e..000000000 --- a/linux/assets/file-icons/icons/folders/folder-components-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-components.svg b/linux/assets/file-icons/icons/folders/folder-components.svg deleted file mode 100644 index 9375049e5..000000000 --- a/linux/assets/file-icons/icons/folders/folder-components.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-config-open.svg b/linux/assets/file-icons/icons/folders/folder-config-open.svg deleted file mode 100644 index 433c9e0e9..000000000 --- a/linux/assets/file-icons/icons/folders/folder-config-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-config.svg b/linux/assets/file-icons/icons/folders/folder-config.svg deleted file mode 100644 index 68e7304c2..000000000 --- a/linux/assets/file-icons/icons/folders/folder-config.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-database-open.svg b/linux/assets/file-icons/icons/folders/folder-database-open.svg deleted file mode 100644 index dcd2fd81a..000000000 --- a/linux/assets/file-icons/icons/folders/folder-database-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-database.svg b/linux/assets/file-icons/icons/folders/folder-database.svg deleted file mode 100644 index 0ed82fe06..000000000 --- a/linux/assets/file-icons/icons/folders/folder-database.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-docs-open.svg b/linux/assets/file-icons/icons/folders/folder-docs-open.svg deleted file mode 100644 index 15024b33e..000000000 --- a/linux/assets/file-icons/icons/folders/folder-docs-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-docs.svg b/linux/assets/file-icons/icons/folders/folder-docs.svg deleted file mode 100644 index 65acb0762..000000000 --- a/linux/assets/file-icons/icons/folders/folder-docs.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-extensions-open.svg b/linux/assets/file-icons/icons/folders/folder-extensions-open.svg deleted file mode 100644 index c07f6810c..000000000 --- a/linux/assets/file-icons/icons/folders/folder-extensions-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-extensions.svg b/linux/assets/file-icons/icons/folders/folder-extensions.svg deleted file mode 100644 index ebcfc4aa0..000000000 --- a/linux/assets/file-icons/icons/folders/folder-extensions.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-git-open.svg b/linux/assets/file-icons/icons/folders/folder-git-open.svg deleted file mode 100644 index df8eca0c3..000000000 --- a/linux/assets/file-icons/icons/folders/folder-git-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-git.svg b/linux/assets/file-icons/icons/folders/folder-git.svg deleted file mode 100644 index 2e6f82e35..000000000 --- a/linux/assets/file-icons/icons/folders/folder-git.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-locales-open.svg b/linux/assets/file-icons/icons/folders/folder-locales-open.svg deleted file mode 100644 index ea2f034f8..000000000 --- a/linux/assets/file-icons/icons/folders/folder-locales-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-locales.svg b/linux/assets/file-icons/icons/folders/folder-locales.svg deleted file mode 100644 index 785df402a..000000000 --- a/linux/assets/file-icons/icons/folders/folder-locales.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-mobile-open.svg b/linux/assets/file-icons/icons/folders/folder-mobile-open.svg deleted file mode 100644 index f1e6fc141..000000000 --- a/linux/assets/file-icons/icons/folders/folder-mobile-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-mobile.svg b/linux/assets/file-icons/icons/folders/folder-mobile.svg deleted file mode 100644 index 6f5dc8122..000000000 --- a/linux/assets/file-icons/icons/folders/folder-mobile.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-open.svg b/linux/assets/file-icons/icons/folders/folder-open.svg deleted file mode 100644 index 44cc2562f..000000000 --- a/linux/assets/file-icons/icons/folders/folder-open.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-packages-open.svg b/linux/assets/file-icons/icons/folders/folder-packages-open.svg deleted file mode 100644 index 372bb7bea..000000000 --- a/linux/assets/file-icons/icons/folders/folder-packages-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-packages.svg b/linux/assets/file-icons/icons/folders/folder-packages.svg deleted file mode 100644 index c60c0c158..000000000 --- a/linux/assets/file-icons/icons/folders/folder-packages.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-routes-open.svg b/linux/assets/file-icons/icons/folders/folder-routes-open.svg deleted file mode 100644 index e11a12e42..000000000 --- a/linux/assets/file-icons/icons/folders/folder-routes-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-routes.svg b/linux/assets/file-icons/icons/folders/folder-routes.svg deleted file mode 100644 index ca413527a..000000000 --- a/linux/assets/file-icons/icons/folders/folder-routes.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-rust-open.svg b/linux/assets/file-icons/icons/folders/folder-rust-open.svg deleted file mode 100644 index a58f44340..000000000 --- a/linux/assets/file-icons/icons/folders/folder-rust-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-rust.svg b/linux/assets/file-icons/icons/folders/folder-rust.svg deleted file mode 100644 index b4d7ad475..000000000 --- a/linux/assets/file-icons/icons/folders/folder-rust.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-scripts-open.svg b/linux/assets/file-icons/icons/folders/folder-scripts-open.svg deleted file mode 100644 index 48be14c05..000000000 --- a/linux/assets/file-icons/icons/folders/folder-scripts-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-scripts.svg b/linux/assets/file-icons/icons/folders/folder-scripts.svg deleted file mode 100644 index af0eae3b8..000000000 --- a/linux/assets/file-icons/icons/folders/folder-scripts.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-security-open.svg b/linux/assets/file-icons/icons/folders/folder-security-open.svg deleted file mode 100644 index f440c6c94..000000000 --- a/linux/assets/file-icons/icons/folders/folder-security-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-security.svg b/linux/assets/file-icons/icons/folders/folder-security.svg deleted file mode 100644 index 7f48358f2..000000000 --- a/linux/assets/file-icons/icons/folders/folder-security.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-source-open.svg b/linux/assets/file-icons/icons/folders/folder-source-open.svg deleted file mode 100644 index 4a3ed1046..000000000 --- a/linux/assets/file-icons/icons/folders/folder-source-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-source.svg b/linux/assets/file-icons/icons/folders/folder-source.svg deleted file mode 100644 index 88bae64e8..000000000 --- a/linux/assets/file-icons/icons/folders/folder-source.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-styles-open.svg b/linux/assets/file-icons/icons/folders/folder-styles-open.svg deleted file mode 100644 index 54fb0735a..000000000 --- a/linux/assets/file-icons/icons/folders/folder-styles-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-styles.svg b/linux/assets/file-icons/icons/folders/folder-styles.svg deleted file mode 100644 index 3bc647e83..000000000 --- a/linux/assets/file-icons/icons/folders/folder-styles.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-test-open.svg b/linux/assets/file-icons/icons/folders/folder-test-open.svg deleted file mode 100644 index 590550958..000000000 --- a/linux/assets/file-icons/icons/folders/folder-test-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder-test.svg b/linux/assets/file-icons/icons/folders/folder-test.svg deleted file mode 100644 index 1de3ba775..000000000 --- a/linux/assets/file-icons/icons/folders/folder-test.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/folders/folder.svg b/linux/assets/file-icons/icons/folders/folder.svg deleted file mode 100644 index 6e0864bb3..000000000 --- a/linux/assets/file-icons/icons/folders/folder.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/linux/assets/file-icons/icons/light/files/adobe.svg b/linux/assets/file-icons/icons/light/files/adobe.svg deleted file mode 100644 index ba716eb30..000000000 --- a/linux/assets/file-icons/icons/light/files/adobe.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - A - diff --git a/linux/assets/file-icons/icons/light/files/agents.svg b/linux/assets/file-icons/icons/light/files/agents.svg deleted file mode 100644 index e787c0597..000000000 --- a/linux/assets/file-icons/icons/light/files/agents.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - AI - diff --git a/linux/assets/file-icons/icons/light/files/android.svg b/linux/assets/file-icons/icons/light/files/android.svg deleted file mode 100644 index f30f85a54..000000000 --- a/linux/assets/file-icons/icons/light/files/android.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/angular.svg b/linux/assets/file-icons/icons/light/files/angular.svg deleted file mode 100644 index 8d0550c61..000000000 --- a/linux/assets/file-icons/icons/light/files/angular.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/ansible.svg b/linux/assets/file-icons/icons/light/files/ansible.svg deleted file mode 100644 index 5b3892010..000000000 --- a/linux/assets/file-icons/icons/light/files/ansible.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - A - diff --git a/linux/assets/file-icons/icons/light/files/apple.svg b/linux/assets/file-icons/icons/light/files/apple.svg deleted file mode 100644 index acba52121..000000000 --- a/linux/assets/file-icons/icons/light/files/apple.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/archive.svg b/linux/assets/file-icons/icons/light/files/archive.svg deleted file mode 100644 index e952d5813..000000000 --- a/linux/assets/file-icons/icons/light/files/archive.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/arduino.svg b/linux/assets/file-icons/icons/light/files/arduino.svg deleted file mode 100644 index 08875c147..000000000 --- a/linux/assets/file-icons/icons/light/files/arduino.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - A - diff --git a/linux/assets/file-icons/icons/light/files/astro.svg b/linux/assets/file-icons/icons/light/files/astro.svg deleted file mode 100644 index 19fd05318..000000000 --- a/linux/assets/file-icons/icons/light/files/astro.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - A - diff --git a/linux/assets/file-icons/icons/light/files/audio.svg b/linux/assets/file-icons/icons/light/files/audio.svg deleted file mode 100644 index 96d9360fd..000000000 --- a/linux/assets/file-icons/icons/light/files/audio.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/authors.svg b/linux/assets/file-icons/icons/light/files/authors.svg deleted file mode 100644 index b408e4921..000000000 --- a/linux/assets/file-icons/icons/light/files/authors.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - BY - diff --git a/linux/assets/file-icons/icons/light/files/babel.svg b/linux/assets/file-icons/icons/light/files/babel.svg deleted file mode 100644 index c3ce53910..000000000 --- a/linux/assets/file-icons/icons/light/files/babel.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - B - diff --git a/linux/assets/file-icons/icons/light/files/biome.svg b/linux/assets/file-icons/icons/light/files/biome.svg deleted file mode 100644 index 7c80ba33b..000000000 --- a/linux/assets/file-icons/icons/light/files/biome.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/bitbucket.svg b/linux/assets/file-icons/icons/light/files/bitbucket.svg deleted file mode 100644 index 7c32ee162..000000000 --- a/linux/assets/file-icons/icons/light/files/bitbucket.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/blender.svg b/linux/assets/file-icons/icons/light/files/blender.svg deleted file mode 100644 index 4153cd689..000000000 --- a/linux/assets/file-icons/icons/light/files/blender.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - B - diff --git a/linux/assets/file-icons/icons/light/files/bun.svg b/linux/assets/file-icons/icons/light/files/bun.svg deleted file mode 100644 index 82af13464..000000000 --- a/linux/assets/file-icons/icons/light/files/bun.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - B - diff --git a/linux/assets/file-icons/icons/light/files/c.svg b/linux/assets/file-icons/icons/light/files/c.svg deleted file mode 100644 index 996ae83d7..000000000 --- a/linux/assets/file-icons/icons/light/files/c.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - C - diff --git a/linux/assets/file-icons/icons/light/files/certificate.svg b/linux/assets/file-icons/icons/light/files/certificate.svg deleted file mode 100644 index 1e5abe60f..000000000 --- a/linux/assets/file-icons/icons/light/files/certificate.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - CRT - diff --git a/linux/assets/file-icons/icons/light/files/changelog.svg b/linux/assets/file-icons/icons/light/files/changelog.svg deleted file mode 100644 index ef01e6c7d..000000000 --- a/linux/assets/file-icons/icons/light/files/changelog.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - LOG - diff --git a/linux/assets/file-icons/icons/light/files/claude.svg b/linux/assets/file-icons/icons/light/files/claude.svg deleted file mode 100644 index 378c9e243..000000000 --- a/linux/assets/file-icons/icons/light/files/claude.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - AI - diff --git a/linux/assets/file-icons/icons/light/files/cline.svg b/linux/assets/file-icons/icons/light/files/cline.svg deleted file mode 100644 index 552832ef4..000000000 --- a/linux/assets/file-icons/icons/light/files/cline.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/clojure.svg b/linux/assets/file-icons/icons/light/files/clojure.svg deleted file mode 100644 index d9521265b..000000000 --- a/linux/assets/file-icons/icons/light/files/clojure.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/cloudflare.svg b/linux/assets/file-icons/icons/light/files/cloudflare.svg deleted file mode 100644 index 97d63b54c..000000000 --- a/linux/assets/file-icons/icons/light/files/cloudflare.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - CF - diff --git a/linux/assets/file-icons/icons/light/files/cmake.svg b/linux/assets/file-icons/icons/light/files/cmake.svg deleted file mode 100644 index cc06991ae..000000000 --- a/linux/assets/file-icons/icons/light/files/cmake.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/codex.svg b/linux/assets/file-icons/icons/light/files/codex.svg deleted file mode 100644 index fd1a89358..000000000 --- a/linux/assets/file-icons/icons/light/files/codex.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/commitlint.svg b/linux/assets/file-icons/icons/light/files/commitlint.svg deleted file mode 100644 index 9566b78f6..000000000 --- a/linux/assets/file-icons/icons/light/files/commitlint.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/config.svg b/linux/assets/file-icons/icons/light/files/config.svg deleted file mode 100644 index ab0ecc327..000000000 --- a/linux/assets/file-icons/icons/light/files/config.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/copilot.svg b/linux/assets/file-icons/icons/light/files/copilot.svg deleted file mode 100644 index a7ef00343..000000000 --- a/linux/assets/file-icons/icons/light/files/copilot.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - AI - diff --git a/linux/assets/file-icons/icons/light/files/cpp.svg b/linux/assets/file-icons/icons/light/files/cpp.svg deleted file mode 100644 index b71ea0cad..000000000 --- a/linux/assets/file-icons/icons/light/files/cpp.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - C+ - diff --git a/linux/assets/file-icons/icons/light/files/csharp.svg b/linux/assets/file-icons/icons/light/files/csharp.svg deleted file mode 100644 index 2f49b3ea6..000000000 --- a/linux/assets/file-icons/icons/light/files/csharp.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - C# - diff --git a/linux/assets/file-icons/icons/light/files/cspell.svg b/linux/assets/file-icons/icons/light/files/cspell.svg deleted file mode 100644 index 1d7e55469..000000000 --- a/linux/assets/file-icons/icons/light/files/cspell.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/css.svg b/linux/assets/file-icons/icons/light/files/css.svg deleted file mode 100644 index 375de6653..000000000 --- a/linux/assets/file-icons/icons/light/files/css.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - # - diff --git a/linux/assets/file-icons/icons/light/files/csv.svg b/linux/assets/file-icons/icons/light/files/csv.svg deleted file mode 100644 index 7ed227c51..000000000 --- a/linux/assets/file-icons/icons/light/files/csv.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - CSV - diff --git a/linux/assets/file-icons/icons/light/files/cursor.svg b/linux/assets/file-icons/icons/light/files/cursor.svg deleted file mode 100644 index df8003f2e..000000000 --- a/linux/assets/file-icons/icons/light/files/cursor.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/cypress.svg b/linux/assets/file-icons/icons/light/files/cypress.svg deleted file mode 100644 index 34e832521..000000000 --- a/linux/assets/file-icons/icons/light/files/cypress.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/dart.svg b/linux/assets/file-icons/icons/light/files/dart.svg deleted file mode 100644 index 941437104..000000000 --- a/linux/assets/file-icons/icons/light/files/dart.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - D - diff --git a/linux/assets/file-icons/icons/light/files/database.svg b/linux/assets/file-icons/icons/light/files/database.svg deleted file mode 100644 index 2040e77f0..000000000 --- a/linux/assets/file-icons/icons/light/files/database.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/deno.svg b/linux/assets/file-icons/icons/light/files/deno.svg deleted file mode 100644 index 301830fce..000000000 --- a/linux/assets/file-icons/icons/light/files/deno.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - D - diff --git a/linux/assets/file-icons/icons/light/files/dependabot.svg b/linux/assets/file-icons/icons/light/files/dependabot.svg deleted file mode 100644 index 38a401d0e..000000000 --- a/linux/assets/file-icons/icons/light/files/dependabot.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - D - diff --git a/linux/assets/file-icons/icons/light/files/devcontainer.svg b/linux/assets/file-icons/icons/light/files/devcontainer.svg deleted file mode 100644 index 9f5dd2277..000000000 --- a/linux/assets/file-icons/icons/light/files/devcontainer.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - DC - diff --git a/linux/assets/file-icons/icons/light/files/diff.svg b/linux/assets/file-icons/icons/light/files/diff.svg deleted file mode 100644 index 6f52d90e4..000000000 --- a/linux/assets/file-icons/icons/light/files/diff.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - +- - diff --git a/linux/assets/file-icons/icons/light/files/django.svg b/linux/assets/file-icons/icons/light/files/django.svg deleted file mode 100644 index 4ec56bcfb..000000000 --- a/linux/assets/file-icons/icons/light/files/django.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/docker-compose.svg b/linux/assets/file-icons/icons/light/files/docker-compose.svg deleted file mode 100644 index 2d4c58925..000000000 --- a/linux/assets/file-icons/icons/light/files/docker-compose.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/docker.svg b/linux/assets/file-icons/icons/light/files/docker.svg deleted file mode 100644 index 867766922..000000000 --- a/linux/assets/file-icons/icons/light/files/docker.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/document.svg b/linux/assets/file-icons/icons/light/files/document.svg deleted file mode 100644 index f5ad58865..000000000 --- a/linux/assets/file-icons/icons/light/files/document.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - DOC - diff --git a/linux/assets/file-icons/icons/light/files/docusaurus.svg b/linux/assets/file-icons/icons/light/files/docusaurus.svg deleted file mode 100644 index ed0fe2214..000000000 --- a/linux/assets/file-icons/icons/light/files/docusaurus.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/drawio.svg b/linux/assets/file-icons/icons/light/files/drawio.svg deleted file mode 100644 index 807b2aa58..000000000 --- a/linux/assets/file-icons/icons/light/files/drawio.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - D - diff --git a/linux/assets/file-icons/icons/light/files/drizzle.svg b/linux/assets/file-icons/icons/light/files/drizzle.svg deleted file mode 100644 index 29a554e29..000000000 --- a/linux/assets/file-icons/icons/light/files/drizzle.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/editorconfig.svg b/linux/assets/file-icons/icons/light/files/editorconfig.svg deleted file mode 100644 index 2532cefd4..000000000 --- a/linux/assets/file-icons/icons/light/files/editorconfig.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/electron.svg b/linux/assets/file-icons/icons/light/files/electron.svg deleted file mode 100644 index 066bddd15..000000000 --- a/linux/assets/file-icons/icons/light/files/electron.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - E - diff --git a/linux/assets/file-icons/icons/light/files/elixir.svg b/linux/assets/file-icons/icons/light/files/elixir.svg deleted file mode 100644 index 70e88edc3..000000000 --- a/linux/assets/file-icons/icons/light/files/elixir.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - EX - diff --git a/linux/assets/file-icons/icons/light/files/env.svg b/linux/assets/file-icons/icons/light/files/env.svg deleted file mode 100644 index 739868f76..000000000 --- a/linux/assets/file-icons/icons/light/files/env.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/erlang.svg b/linux/assets/file-icons/icons/light/files/erlang.svg deleted file mode 100644 index 6dd8f7210..000000000 --- a/linux/assets/file-icons/icons/light/files/erlang.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - ER - diff --git a/linux/assets/file-icons/icons/light/files/error.svg b/linux/assets/file-icons/icons/light/files/error.svg deleted file mode 100644 index 6777185ab..000000000 --- a/linux/assets/file-icons/icons/light/files/error.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/eslint.svg b/linux/assets/file-icons/icons/light/files/eslint.svg deleted file mode 100644 index 31cdc71f1..000000000 --- a/linux/assets/file-icons/icons/light/files/eslint.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - E - diff --git a/linux/assets/file-icons/icons/light/files/excalidraw.svg b/linux/assets/file-icons/icons/light/files/excalidraw.svg deleted file mode 100644 index b3ab80975..000000000 --- a/linux/assets/file-icons/icons/light/files/excalidraw.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/fastapi.svg b/linux/assets/file-icons/icons/light/files/fastapi.svg deleted file mode 100644 index cf4cb4448..000000000 --- a/linux/assets/file-icons/icons/light/files/fastapi.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - FA - diff --git a/linux/assets/file-icons/icons/light/files/figma.svg b/linux/assets/file-icons/icons/light/files/figma.svg deleted file mode 100644 index 9d3f20dfc..000000000 --- a/linux/assets/file-icons/icons/light/files/figma.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - F - diff --git a/linux/assets/file-icons/icons/light/files/file.svg b/linux/assets/file-icons/icons/light/files/file.svg deleted file mode 100644 index ecd9c5967..000000000 --- a/linux/assets/file-icons/icons/light/files/file.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/firebase.svg b/linux/assets/file-icons/icons/light/files/firebase.svg deleted file mode 100644 index 160cd7a47..000000000 --- a/linux/assets/file-icons/icons/light/files/firebase.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/flask.svg b/linux/assets/file-icons/icons/light/files/flask.svg deleted file mode 100644 index f91b6630f..000000000 --- a/linux/assets/file-icons/icons/light/files/flask.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/font.svg b/linux/assets/file-icons/icons/light/files/font.svg deleted file mode 100644 index 232442e9e..000000000 --- a/linux/assets/file-icons/icons/light/files/font.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/gatsby.svg b/linux/assets/file-icons/icons/light/files/gatsby.svg deleted file mode 100644 index f0ccaaf81..000000000 --- a/linux/assets/file-icons/icons/light/files/gatsby.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - G - diff --git a/linux/assets/file-icons/icons/light/files/gemini.svg b/linux/assets/file-icons/icons/light/files/gemini.svg deleted file mode 100644 index aa1b35013..000000000 --- a/linux/assets/file-icons/icons/light/files/gemini.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - G - diff --git a/linux/assets/file-icons/icons/light/files/git.svg b/linux/assets/file-icons/icons/light/files/git.svg deleted file mode 100644 index 60e5c97fe..000000000 --- a/linux/assets/file-icons/icons/light/files/git.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/github-actions.svg b/linux/assets/file-icons/icons/light/files/github-actions.svg deleted file mode 100644 index 1a8595f25..000000000 --- a/linux/assets/file-icons/icons/light/files/github-actions.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - GH - diff --git a/linux/assets/file-icons/icons/light/files/github.svg b/linux/assets/file-icons/icons/light/files/github.svg deleted file mode 100644 index ac94fb622..000000000 --- a/linux/assets/file-icons/icons/light/files/github.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/gitlab.svg b/linux/assets/file-icons/icons/light/files/gitlab.svg deleted file mode 100644 index eb6eb35ed..000000000 --- a/linux/assets/file-icons/icons/light/files/gitlab.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/go.svg b/linux/assets/file-icons/icons/light/files/go.svg deleted file mode 100644 index 73f3b11a9..000000000 --- a/linux/assets/file-icons/icons/light/files/go.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/gradle.svg b/linux/assets/file-icons/icons/light/files/gradle.svg deleted file mode 100644 index 456555037..000000000 --- a/linux/assets/file-icons/icons/light/files/gradle.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - G - diff --git a/linux/assets/file-icons/icons/light/files/graphql-schema.svg b/linux/assets/file-icons/icons/light/files/graphql-schema.svg deleted file mode 100644 index d50cccb17..000000000 --- a/linux/assets/file-icons/icons/light/files/graphql-schema.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - GS - diff --git a/linux/assets/file-icons/icons/light/files/graphql.svg b/linux/assets/file-icons/icons/light/files/graphql.svg deleted file mode 100644 index c63624564..000000000 --- a/linux/assets/file-icons/icons/light/files/graphql.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - G - diff --git a/linux/assets/file-icons/icons/light/files/haskell.svg b/linux/assets/file-icons/icons/light/files/haskell.svg deleted file mode 100644 index 24db0ded5..000000000 --- a/linux/assets/file-icons/icons/light/files/haskell.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - HS - diff --git a/linux/assets/file-icons/icons/light/files/helm.svg b/linux/assets/file-icons/icons/light/files/helm.svg deleted file mode 100644 index f1c3bb82a..000000000 --- a/linux/assets/file-icons/icons/light/files/helm.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - H - diff --git a/linux/assets/file-icons/icons/light/files/html.svg b/linux/assets/file-icons/icons/light/files/html.svg deleted file mode 100644 index d1e8d808b..000000000 --- a/linux/assets/file-icons/icons/light/files/html.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - H - diff --git a/linux/assets/file-icons/icons/light/files/http.svg b/linux/assets/file-icons/icons/light/files/http.svg deleted file mode 100644 index 977a063c4..000000000 --- a/linux/assets/file-icons/icons/light/files/http.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - HT - diff --git a/linux/assets/file-icons/icons/light/files/hurl.svg b/linux/assets/file-icons/icons/light/files/hurl.svg deleted file mode 100644 index 1ce145c73..000000000 --- a/linux/assets/file-icons/icons/light/files/hurl.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - HU - diff --git a/linux/assets/file-icons/icons/light/files/image.svg b/linux/assets/file-icons/icons/light/files/image.svg deleted file mode 100644 index 2bfa5eb89..000000000 --- a/linux/assets/file-icons/icons/light/files/image.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/index-js.svg b/linux/assets/file-icons/icons/light/files/index-js.svg deleted file mode 100644 index e676bf67e..000000000 --- a/linux/assets/file-icons/icons/light/files/index-js.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - IDX - diff --git a/linux/assets/file-icons/icons/light/files/index-ts.svg b/linux/assets/file-icons/icons/light/files/index-ts.svg deleted file mode 100644 index 9c62ff182..000000000 --- a/linux/assets/file-icons/icons/light/files/index-ts.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - IDX - diff --git a/linux/assets/file-icons/icons/light/files/java.svg b/linux/assets/file-icons/icons/light/files/java.svg deleted file mode 100644 index 948e01ddd..000000000 --- a/linux/assets/file-icons/icons/light/files/java.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/javascript.svg b/linux/assets/file-icons/icons/light/files/javascript.svg deleted file mode 100644 index 0e36f8cfe..000000000 --- a/linux/assets/file-icons/icons/light/files/javascript.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - JS - diff --git a/linux/assets/file-icons/icons/light/files/jenkins.svg b/linux/assets/file-icons/icons/light/files/jenkins.svg deleted file mode 100644 index cc549fc11..000000000 --- a/linux/assets/file-icons/icons/light/files/jenkins.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/jest.svg b/linux/assets/file-icons/icons/light/files/jest.svg deleted file mode 100644 index 97aef2580..000000000 --- a/linux/assets/file-icons/icons/light/files/jest.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/jsconfig.svg b/linux/assets/file-icons/icons/light/files/jsconfig.svg deleted file mode 100644 index 2af749c4d..000000000 --- a/linux/assets/file-icons/icons/light/files/jsconfig.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/json.svg b/linux/assets/file-icons/icons/light/files/json.svg deleted file mode 100644 index 9bd3910d1..000000000 --- a/linux/assets/file-icons/icons/light/files/json.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - {} - diff --git a/linux/assets/file-icons/icons/light/files/julia.svg b/linux/assets/file-icons/icons/light/files/julia.svg deleted file mode 100644 index 1400b22df..000000000 --- a/linux/assets/file-icons/icons/light/files/julia.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - JL - diff --git a/linux/assets/file-icons/icons/light/files/key.svg b/linux/assets/file-icons/icons/light/files/key.svg deleted file mode 100644 index 48a792e87..000000000 --- a/linux/assets/file-icons/icons/light/files/key.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/kotlin.svg b/linux/assets/file-icons/icons/light/files/kotlin.svg deleted file mode 100644 index 8d0fd4dd0..000000000 --- a/linux/assets/file-icons/icons/light/files/kotlin.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - K - diff --git a/linux/assets/file-icons/icons/light/files/kubernetes.svg b/linux/assets/file-icons/icons/light/files/kubernetes.svg deleted file mode 100644 index 6837d0a06..000000000 --- a/linux/assets/file-icons/icons/light/files/kubernetes.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - K8 - diff --git a/linux/assets/file-icons/icons/light/files/laravel.svg b/linux/assets/file-icons/icons/light/files/laravel.svg deleted file mode 100644 index 8c2f3393e..000000000 --- a/linux/assets/file-icons/icons/light/files/laravel.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/layout.svg b/linux/assets/file-icons/icons/light/files/layout.svg deleted file mode 100644 index 70c99e58b..000000000 --- a/linux/assets/file-icons/icons/light/files/layout.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - L - diff --git a/linux/assets/file-icons/icons/light/files/license.svg b/linux/assets/file-icons/icons/light/files/license.svg deleted file mode 100644 index 0aa5fbce7..000000000 --- a/linux/assets/file-icons/icons/light/files/license.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - LIC - diff --git a/linux/assets/file-icons/icons/light/files/lintstaged.svg b/linux/assets/file-icons/icons/light/files/lintstaged.svg deleted file mode 100644 index 0caeb0f9f..000000000 --- a/linux/assets/file-icons/icons/light/files/lintstaged.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - LS - diff --git a/linux/assets/file-icons/icons/light/files/linux.svg b/linux/assets/file-icons/icons/light/files/linux.svg deleted file mode 100644 index b43691eb4..000000000 --- a/linux/assets/file-icons/icons/light/files/linux.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/lit.svg b/linux/assets/file-icons/icons/light/files/lit.svg deleted file mode 100644 index 76e58c410..000000000 --- a/linux/assets/file-icons/icons/light/files/lit.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/lithe.svg b/linux/assets/file-icons/icons/light/files/lithe.svg deleted file mode 100644 index dc5523ab4..000000000 --- a/linux/assets/file-icons/icons/light/files/lithe.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - L - diff --git a/linux/assets/file-icons/icons/light/files/loading.svg b/linux/assets/file-icons/icons/light/files/loading.svg deleted file mode 100644 index cb632d555..000000000 --- a/linux/assets/file-icons/icons/light/files/loading.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - ... - diff --git a/linux/assets/file-icons/icons/light/files/lock.svg b/linux/assets/file-icons/icons/light/files/lock.svg deleted file mode 100644 index 8abab2a1d..000000000 --- a/linux/assets/file-icons/icons/light/files/lock.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/log.svg b/linux/assets/file-icons/icons/light/files/log.svg deleted file mode 100644 index f06880824..000000000 --- a/linux/assets/file-icons/icons/light/files/log.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - LOG - diff --git a/linux/assets/file-icons/icons/light/files/lua.svg b/linux/assets/file-icons/icons/light/files/lua.svg deleted file mode 100644 index 3fc5f6d9b..000000000 --- a/linux/assets/file-icons/icons/light/files/lua.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - L - diff --git a/linux/assets/file-icons/icons/light/files/makefile.svg b/linux/assets/file-icons/icons/light/files/makefile.svg deleted file mode 100644 index e6ca2fb8b..000000000 --- a/linux/assets/file-icons/icons/light/files/makefile.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/markdown.svg b/linux/assets/file-icons/icons/light/files/markdown.svg deleted file mode 100644 index 0a57e1538..000000000 --- a/linux/assets/file-icons/icons/light/files/markdown.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/markdownlint.svg b/linux/assets/file-icons/icons/light/files/markdownlint.svg deleted file mode 100644 index 3aabe5da5..000000000 --- a/linux/assets/file-icons/icons/light/files/markdownlint.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/maven.svg b/linux/assets/file-icons/icons/light/files/maven.svg deleted file mode 100644 index b5d240b51..000000000 --- a/linux/assets/file-icons/icons/light/files/maven.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - M - diff --git a/linux/assets/file-icons/icons/light/files/mcp.svg b/linux/assets/file-icons/icons/light/files/mcp.svg deleted file mode 100644 index 9480e0499..000000000 --- a/linux/assets/file-icons/icons/light/files/mcp.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - M - diff --git a/linux/assets/file-icons/icons/light/files/mermaid.svg b/linux/assets/file-icons/icons/light/files/mermaid.svg deleted file mode 100644 index 4d45d6f3c..000000000 --- a/linux/assets/file-icons/icons/light/files/mermaid.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - MM - diff --git a/linux/assets/file-icons/icons/light/files/mongo.svg b/linux/assets/file-icons/icons/light/files/mongo.svg deleted file mode 100644 index 18986c84c..000000000 --- a/linux/assets/file-icons/icons/light/files/mongo.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/netlify.svg b/linux/assets/file-icons/icons/light/files/netlify.svg deleted file mode 100644 index 941d09eb5..000000000 --- a/linux/assets/file-icons/icons/light/files/netlify.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - N - diff --git a/linux/assets/file-icons/icons/light/files/next.svg b/linux/assets/file-icons/icons/light/files/next.svg deleted file mode 100644 index 6b62626f4..000000000 --- a/linux/assets/file-icons/icons/light/files/next.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - N - diff --git a/linux/assets/file-icons/icons/light/files/nginx.svg b/linux/assets/file-icons/icons/light/files/nginx.svg deleted file mode 100644 index e56efaeea..000000000 --- a/linux/assets/file-icons/icons/light/files/nginx.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - N - diff --git a/linux/assets/file-icons/icons/light/files/nim.svg b/linux/assets/file-icons/icons/light/files/nim.svg deleted file mode 100644 index 8fb2d6ee4..000000000 --- a/linux/assets/file-icons/icons/light/files/nim.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - N - diff --git a/linux/assets/file-icons/icons/light/files/nix.svg b/linux/assets/file-icons/icons/light/files/nix.svg deleted file mode 100644 index 5e4119b95..000000000 --- a/linux/assets/file-icons/icons/light/files/nix.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - N - diff --git a/linux/assets/file-icons/icons/light/files/node.svg b/linux/assets/file-icons/icons/light/files/node.svg deleted file mode 100644 index 6bc1f64b2..000000000 --- a/linux/assets/file-icons/icons/light/files/node.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - N - diff --git a/linux/assets/file-icons/icons/light/files/not-found.svg b/linux/assets/file-icons/icons/light/files/not-found.svg deleted file mode 100644 index 0062bda71..000000000 --- a/linux/assets/file-icons/icons/light/files/not-found.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/notebook.svg b/linux/assets/file-icons/icons/light/files/notebook.svg deleted file mode 100644 index a9c69987e..000000000 --- a/linux/assets/file-icons/icons/light/files/notebook.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/npm.svg b/linux/assets/file-icons/icons/light/files/npm.svg deleted file mode 100644 index bfb1828ca..000000000 --- a/linux/assets/file-icons/icons/light/files/npm.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - N - diff --git a/linux/assets/file-icons/icons/light/files/nuxt.svg b/linux/assets/file-icons/icons/light/files/nuxt.svg deleted file mode 100644 index d169e54d3..000000000 --- a/linux/assets/file-icons/icons/light/files/nuxt.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - N - diff --git a/linux/assets/file-icons/icons/light/files/nx.svg b/linux/assets/file-icons/icons/light/files/nx.svg deleted file mode 100644 index 69e48c5c8..000000000 --- a/linux/assets/file-icons/icons/light/files/nx.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - NX - diff --git a/linux/assets/file-icons/icons/light/files/ocaml.svg b/linux/assets/file-icons/icons/light/files/ocaml.svg deleted file mode 100644 index 57d304597..000000000 --- a/linux/assets/file-icons/icons/light/files/ocaml.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - ML - diff --git a/linux/assets/file-icons/icons/light/files/package.svg b/linux/assets/file-icons/icons/light/files/package.svg deleted file mode 100644 index 21909634f..000000000 --- a/linux/assets/file-icons/icons/light/files/package.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/page.svg b/linux/assets/file-icons/icons/light/files/page.svg deleted file mode 100644 index 6832c7701..000000000 --- a/linux/assets/file-icons/icons/light/files/page.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - P - diff --git a/linux/assets/file-icons/icons/light/files/patch.svg b/linux/assets/file-icons/icons/light/files/patch.svg deleted file mode 100644 index 5c1c448a2..000000000 --- a/linux/assets/file-icons/icons/light/files/patch.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - +- - diff --git a/linux/assets/file-icons/icons/light/files/pdf.svg b/linux/assets/file-icons/icons/light/files/pdf.svg deleted file mode 100644 index e79c22e9b..000000000 --- a/linux/assets/file-icons/icons/light/files/pdf.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - PDF - diff --git a/linux/assets/file-icons/icons/light/files/perl.svg b/linux/assets/file-icons/icons/light/files/perl.svg deleted file mode 100644 index a41c0cacd..000000000 --- a/linux/assets/file-icons/icons/light/files/perl.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - PL - diff --git a/linux/assets/file-icons/icons/light/files/php.svg b/linux/assets/file-icons/icons/light/files/php.svg deleted file mode 100644 index d4c2edfe9..000000000 --- a/linux/assets/file-icons/icons/light/files/php.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - P - diff --git a/linux/assets/file-icons/icons/light/files/playwright.svg b/linux/assets/file-icons/icons/light/files/playwright.svg deleted file mode 100644 index 0317c2886..000000000 --- a/linux/assets/file-icons/icons/light/files/playwright.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/pnpm.svg b/linux/assets/file-icons/icons/light/files/pnpm.svg deleted file mode 100644 index 03d12c964..000000000 --- a/linux/assets/file-icons/icons/light/files/pnpm.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - P - diff --git a/linux/assets/file-icons/icons/light/files/postgres.svg b/linux/assets/file-icons/icons/light/files/postgres.svg deleted file mode 100644 index 368daa77f..000000000 --- a/linux/assets/file-icons/icons/light/files/postgres.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/powerpoint.svg b/linux/assets/file-icons/icons/light/files/powerpoint.svg deleted file mode 100644 index 2e8a55782..000000000 --- a/linux/assets/file-icons/icons/light/files/powerpoint.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - PPT - diff --git a/linux/assets/file-icons/icons/light/files/prettier.svg b/linux/assets/file-icons/icons/light/files/prettier.svg deleted file mode 100644 index 8f24fa2de..000000000 --- a/linux/assets/file-icons/icons/light/files/prettier.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/prisma.svg b/linux/assets/file-icons/icons/light/files/prisma.svg deleted file mode 100644 index 3a575c6c6..000000000 --- a/linux/assets/file-icons/icons/light/files/prisma.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/proto.svg b/linux/assets/file-icons/icons/light/files/proto.svg deleted file mode 100644 index 6551ba995..000000000 --- a/linux/assets/file-icons/icons/light/files/proto.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - P - diff --git a/linux/assets/file-icons/icons/light/files/python.svg b/linux/assets/file-icons/icons/light/files/python.svg deleted file mode 100644 index 72f09abe9..000000000 --- a/linux/assets/file-icons/icons/light/files/python.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/qwik.svg b/linux/assets/file-icons/icons/light/files/qwik.svg deleted file mode 100644 index 5e09fe0eb..000000000 --- a/linux/assets/file-icons/icons/light/files/qwik.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - Q - diff --git a/linux/assets/file-icons/icons/light/files/r.svg b/linux/assets/file-icons/icons/light/files/r.svg deleted file mode 100644 index b496fd493..000000000 --- a/linux/assets/file-icons/icons/light/files/r.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - R - diff --git a/linux/assets/file-icons/icons/light/files/react.svg b/linux/assets/file-icons/icons/light/files/react.svg deleted file mode 100644 index aac59a6c4..000000000 --- a/linux/assets/file-icons/icons/light/files/react.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/redis.svg b/linux/assets/file-icons/icons/light/files/redis.svg deleted file mode 100644 index 7cd4c2f24..000000000 --- a/linux/assets/file-icons/icons/light/files/redis.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/remix.svg b/linux/assets/file-icons/icons/light/files/remix.svg deleted file mode 100644 index 975fda0a2..000000000 --- a/linux/assets/file-icons/icons/light/files/remix.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - R - diff --git a/linux/assets/file-icons/icons/light/files/renovate.svg b/linux/assets/file-icons/icons/light/files/renovate.svg deleted file mode 100644 index a95016811..000000000 --- a/linux/assets/file-icons/icons/light/files/renovate.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/rescript.svg b/linux/assets/file-icons/icons/light/files/rescript.svg deleted file mode 100644 index eb898c1d1..000000000 --- a/linux/assets/file-icons/icons/light/files/rescript.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - RE - diff --git a/linux/assets/file-icons/icons/light/files/rollup.svg b/linux/assets/file-icons/icons/light/files/rollup.svg deleted file mode 100644 index 8c846b294..000000000 --- a/linux/assets/file-icons/icons/light/files/rollup.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - R - diff --git a/linux/assets/file-icons/icons/light/files/route.svg b/linux/assets/file-icons/icons/light/files/route.svg deleted file mode 100644 index a1db3d411..000000000 --- a/linux/assets/file-icons/icons/light/files/route.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - RT - diff --git a/linux/assets/file-icons/icons/light/files/rspack.svg b/linux/assets/file-icons/icons/light/files/rspack.svg deleted file mode 100644 index a33654646..000000000 --- a/linux/assets/file-icons/icons/light/files/rspack.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - R - diff --git a/linux/assets/file-icons/icons/light/files/ruby.svg b/linux/assets/file-icons/icons/light/files/ruby.svg deleted file mode 100644 index d245a2255..000000000 --- a/linux/assets/file-icons/icons/light/files/ruby.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - RB - diff --git a/linux/assets/file-icons/icons/light/files/rust.svg b/linux/assets/file-icons/icons/light/files/rust.svg deleted file mode 100644 index 519da0c0d..000000000 --- a/linux/assets/file-icons/icons/light/files/rust.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/sass.svg b/linux/assets/file-icons/icons/light/files/sass.svg deleted file mode 100644 index 753e00605..000000000 --- a/linux/assets/file-icons/icons/light/files/sass.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - S - diff --git a/linux/assets/file-icons/icons/light/files/scala.svg b/linux/assets/file-icons/icons/light/files/scala.svg deleted file mode 100644 index f90003510..000000000 --- a/linux/assets/file-icons/icons/light/files/scala.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - S - diff --git a/linux/assets/file-icons/icons/light/files/security.svg b/linux/assets/file-icons/icons/light/files/security.svg deleted file mode 100644 index 349ea6213..000000000 --- a/linux/assets/file-icons/icons/light/files/security.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - SEC - diff --git a/linux/assets/file-icons/icons/light/files/shell.svg b/linux/assets/file-icons/icons/light/files/shell.svg deleted file mode 100644 index 90d4ad418..000000000 --- a/linux/assets/file-icons/icons/light/files/shell.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/sketch.svg b/linux/assets/file-icons/icons/light/files/sketch.svg deleted file mode 100644 index 740150901..000000000 --- a/linux/assets/file-icons/icons/light/files/sketch.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - S - diff --git a/linux/assets/file-icons/icons/light/files/solid.svg b/linux/assets/file-icons/icons/light/files/solid.svg deleted file mode 100644 index e0d41430a..000000000 --- a/linux/assets/file-icons/icons/light/files/solid.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - S - diff --git a/linux/assets/file-icons/icons/light/files/solidity.svg b/linux/assets/file-icons/icons/light/files/solidity.svg deleted file mode 100644 index 646c19201..000000000 --- a/linux/assets/file-icons/icons/light/files/solidity.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - S - diff --git a/linux/assets/file-icons/icons/light/files/spreadsheet.svg b/linux/assets/file-icons/icons/light/files/spreadsheet.svg deleted file mode 100644 index 72a4c1e5e..000000000 --- a/linux/assets/file-icons/icons/light/files/spreadsheet.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - XLS - diff --git a/linux/assets/file-icons/icons/light/files/sql.svg b/linux/assets/file-icons/icons/light/files/sql.svg deleted file mode 100644 index dbaad7bf8..000000000 --- a/linux/assets/file-icons/icons/light/files/sql.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/storybook.svg b/linux/assets/file-icons/icons/light/files/storybook.svg deleted file mode 100644 index 23f0d98b4..000000000 --- a/linux/assets/file-icons/icons/light/files/storybook.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/stylelint.svg b/linux/assets/file-icons/icons/light/files/stylelint.svg deleted file mode 100644 index bfca7276e..000000000 --- a/linux/assets/file-icons/icons/light/files/stylelint.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - SL - diff --git a/linux/assets/file-icons/icons/light/files/supabase.svg b/linux/assets/file-icons/icons/light/files/supabase.svg deleted file mode 100644 index 4d37adde0..000000000 --- a/linux/assets/file-icons/icons/light/files/supabase.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/svelte.svg b/linux/assets/file-icons/icons/light/files/svelte.svg deleted file mode 100644 index 2ca6817c6..000000000 --- a/linux/assets/file-icons/icons/light/files/svelte.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - S - diff --git a/linux/assets/file-icons/icons/light/files/svg.svg b/linux/assets/file-icons/icons/light/files/svg.svg deleted file mode 100644 index 360f68910..000000000 --- a/linux/assets/file-icons/icons/light/files/svg.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - S - diff --git a/linux/assets/file-icons/icons/light/files/swc.svg b/linux/assets/file-icons/icons/light/files/swc.svg deleted file mode 100644 index 79eb8ecfb..000000000 --- a/linux/assets/file-icons/icons/light/files/swc.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - S - diff --git a/linux/assets/file-icons/icons/light/files/swift.svg b/linux/assets/file-icons/icons/light/files/swift.svg deleted file mode 100644 index 80d17c02c..000000000 --- a/linux/assets/file-icons/icons/light/files/swift.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/tailwind.svg b/linux/assets/file-icons/icons/light/files/tailwind.svg deleted file mode 100644 index c6b21b1d7..000000000 --- a/linux/assets/file-icons/icons/light/files/tailwind.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - T - diff --git a/linux/assets/file-icons/icons/light/files/tauri.svg b/linux/assets/file-icons/icons/light/files/tauri.svg deleted file mode 100644 index 9a7394cb7..000000000 --- a/linux/assets/file-icons/icons/light/files/tauri.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/terraform.svg b/linux/assets/file-icons/icons/light/files/terraform.svg deleted file mode 100644 index 6318401f8..000000000 --- a/linux/assets/file-icons/icons/light/files/terraform.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - TF - diff --git a/linux/assets/file-icons/icons/light/files/test.svg b/linux/assets/file-icons/icons/light/files/test.svg deleted file mode 100644 index fb610d1ea..000000000 --- a/linux/assets/file-icons/icons/light/files/test.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/text.svg b/linux/assets/file-icons/icons/light/files/text.svg deleted file mode 100644 index 7755a7710..000000000 --- a/linux/assets/file-icons/icons/light/files/text.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - TXT - diff --git a/linux/assets/file-icons/icons/light/files/toml.svg b/linux/assets/file-icons/icons/light/files/toml.svg deleted file mode 100644 index 96fb34215..000000000 --- a/linux/assets/file-icons/icons/light/files/toml.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/turborepo.svg b/linux/assets/file-icons/icons/light/files/turborepo.svg deleted file mode 100644 index 6ef2337d7..000000000 --- a/linux/assets/file-icons/icons/light/files/turborepo.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - T - diff --git a/linux/assets/file-icons/icons/light/files/typescript.svg b/linux/assets/file-icons/icons/light/files/typescript.svg deleted file mode 100644 index d52a028e3..000000000 --- a/linux/assets/file-icons/icons/light/files/typescript.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - TS - diff --git a/linux/assets/file-icons/icons/light/files/vercel-config.svg b/linux/assets/file-icons/icons/light/files/vercel-config.svg deleted file mode 100644 index 6c5eb2b1f..000000000 --- a/linux/assets/file-icons/icons/light/files/vercel-config.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - VC - diff --git a/linux/assets/file-icons/icons/light/files/vercel.svg b/linux/assets/file-icons/icons/light/files/vercel.svg deleted file mode 100644 index 64752bab9..000000000 --- a/linux/assets/file-icons/icons/light/files/vercel.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - V - diff --git a/linux/assets/file-icons/icons/light/files/video.svg b/linux/assets/file-icons/icons/light/files/video.svg deleted file mode 100644 index 86cd60bb3..000000000 --- a/linux/assets/file-icons/icons/light/files/video.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/vite.svg b/linux/assets/file-icons/icons/light/files/vite.svg deleted file mode 100644 index 3a4e803c6..000000000 --- a/linux/assets/file-icons/icons/light/files/vite.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - V - diff --git a/linux/assets/file-icons/icons/light/files/vitest.svg b/linux/assets/file-icons/icons/light/files/vitest.svg deleted file mode 100644 index cc4872157..000000000 --- a/linux/assets/file-icons/icons/light/files/vitest.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/vue.svg b/linux/assets/file-icons/icons/light/files/vue.svg deleted file mode 100644 index 644da25b9..000000000 --- a/linux/assets/file-icons/icons/light/files/vue.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - V - diff --git a/linux/assets/file-icons/icons/light/files/warning.svg b/linux/assets/file-icons/icons/light/files/warning.svg deleted file mode 100644 index 9e9cb6330..000000000 --- a/linux/assets/file-icons/icons/light/files/warning.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/wasm.svg b/linux/assets/file-icons/icons/light/files/wasm.svg deleted file mode 100644 index 3a63d47a9..000000000 --- a/linux/assets/file-icons/icons/light/files/wasm.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - W - diff --git a/linux/assets/file-icons/icons/light/files/webpack.svg b/linux/assets/file-icons/icons/light/files/webpack.svg deleted file mode 100644 index 14cb684b2..000000000 --- a/linux/assets/file-icons/icons/light/files/webpack.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - W - diff --git a/linux/assets/file-icons/icons/light/files/windows.svg b/linux/assets/file-icons/icons/light/files/windows.svg deleted file mode 100644 index 92afb257d..000000000 --- a/linux/assets/file-icons/icons/light/files/windows.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - W - diff --git a/linux/assets/file-icons/icons/light/files/word.svg b/linux/assets/file-icons/icons/light/files/word.svg deleted file mode 100644 index 7f54bb1fc..000000000 --- a/linux/assets/file-icons/icons/light/files/word.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - DOC - diff --git a/linux/assets/file-icons/icons/light/files/xcode.svg b/linux/assets/file-icons/icons/light/files/xcode.svg deleted file mode 100644 index 51a37e666..000000000 --- a/linux/assets/file-icons/icons/light/files/xcode.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/files/xml.svg b/linux/assets/file-icons/icons/light/files/xml.svg deleted file mode 100644 index f1c38169f..000000000 --- a/linux/assets/file-icons/icons/light/files/xml.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - <> - diff --git a/linux/assets/file-icons/icons/light/files/yaml.svg b/linux/assets/file-icons/icons/light/files/yaml.svg deleted file mode 100644 index 8465e7e70..000000000 --- a/linux/assets/file-icons/icons/light/files/yaml.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - Y - diff --git a/linux/assets/file-icons/icons/light/files/yarn.svg b/linux/assets/file-icons/icons/light/files/yarn.svg deleted file mode 100644 index 05219d53a..000000000 --- a/linux/assets/file-icons/icons/light/files/yarn.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - Y - diff --git a/linux/assets/file-icons/icons/light/files/zig.svg b/linux/assets/file-icons/icons/light/files/zig.svg deleted file mode 100644 index c529bd03a..000000000 --- a/linux/assets/file-icons/icons/light/files/zig.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-ai-open.svg b/linux/assets/file-icons/icons/light/folders/folder-ai-open.svg deleted file mode 100644 index 5e951884f..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-ai-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-ai.svg b/linux/assets/file-icons/icons/light/folders/folder-ai.svg deleted file mode 100644 index 44f8c72aa..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-ai.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-assets-open.svg b/linux/assets/file-icons/icons/light/folders/folder-assets-open.svg deleted file mode 100644 index 88538645e..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-assets-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-assets.svg b/linux/assets/file-icons/icons/light/folders/folder-assets.svg deleted file mode 100644 index 94d68d0b7..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-assets.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-build-open.svg b/linux/assets/file-icons/icons/light/folders/folder-build-open.svg deleted file mode 100644 index 690053176..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-build-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-build.svg b/linux/assets/file-icons/icons/light/folders/folder-build.svg deleted file mode 100644 index 8e17e72f2..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-build.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-cloud-open.svg b/linux/assets/file-icons/icons/light/folders/folder-cloud-open.svg deleted file mode 100644 index 52f4a926a..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-cloud-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-cloud.svg b/linux/assets/file-icons/icons/light/folders/folder-cloud.svg deleted file mode 100644 index c0bce4b87..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-cloud.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-components-open.svg b/linux/assets/file-icons/icons/light/folders/folder-components-open.svg deleted file mode 100644 index 7ddb8b0d3..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-components-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-components.svg b/linux/assets/file-icons/icons/light/folders/folder-components.svg deleted file mode 100644 index 95b2529da..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-components.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-config-open.svg b/linux/assets/file-icons/icons/light/folders/folder-config-open.svg deleted file mode 100644 index b2bd27620..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-config-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-config.svg b/linux/assets/file-icons/icons/light/folders/folder-config.svg deleted file mode 100644 index 2a9817835..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-config.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-database-open.svg b/linux/assets/file-icons/icons/light/folders/folder-database-open.svg deleted file mode 100644 index 6755b5694..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-database-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-database.svg b/linux/assets/file-icons/icons/light/folders/folder-database.svg deleted file mode 100644 index 8ac9462ab..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-database.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-docs-open.svg b/linux/assets/file-icons/icons/light/folders/folder-docs-open.svg deleted file mode 100644 index f244dc895..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-docs-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-docs.svg b/linux/assets/file-icons/icons/light/folders/folder-docs.svg deleted file mode 100644 index 88f62bd59..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-docs.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-extensions-open.svg b/linux/assets/file-icons/icons/light/folders/folder-extensions-open.svg deleted file mode 100644 index 3119706f1..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-extensions-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-extensions.svg b/linux/assets/file-icons/icons/light/folders/folder-extensions.svg deleted file mode 100644 index 2e24257d9..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-extensions.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-git-open.svg b/linux/assets/file-icons/icons/light/folders/folder-git-open.svg deleted file mode 100644 index 946e9cdeb..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-git-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-git.svg b/linux/assets/file-icons/icons/light/folders/folder-git.svg deleted file mode 100644 index f0b06065a..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-git.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-locales-open.svg b/linux/assets/file-icons/icons/light/folders/folder-locales-open.svg deleted file mode 100644 index 73c3d1386..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-locales-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-locales.svg b/linux/assets/file-icons/icons/light/folders/folder-locales.svg deleted file mode 100644 index 6fc668015..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-locales.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-mobile-open.svg b/linux/assets/file-icons/icons/light/folders/folder-mobile-open.svg deleted file mode 100644 index 243086c22..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-mobile-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-mobile.svg b/linux/assets/file-icons/icons/light/folders/folder-mobile.svg deleted file mode 100644 index 7fa4504b5..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-mobile.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-open.svg b/linux/assets/file-icons/icons/light/folders/folder-open.svg deleted file mode 100644 index 2dc12ef87..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-open.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-packages-open.svg b/linux/assets/file-icons/icons/light/folders/folder-packages-open.svg deleted file mode 100644 index 1c7e10af4..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-packages-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-packages.svg b/linux/assets/file-icons/icons/light/folders/folder-packages.svg deleted file mode 100644 index 0663dd214..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-packages.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-routes-open.svg b/linux/assets/file-icons/icons/light/folders/folder-routes-open.svg deleted file mode 100644 index 2e6036a73..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-routes-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-routes.svg b/linux/assets/file-icons/icons/light/folders/folder-routes.svg deleted file mode 100644 index c88e28f75..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-routes.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-rust-open.svg b/linux/assets/file-icons/icons/light/folders/folder-rust-open.svg deleted file mode 100644 index 297e2377c..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-rust-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-rust.svg b/linux/assets/file-icons/icons/light/folders/folder-rust.svg deleted file mode 100644 index 35e5e2327..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-rust.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-scripts-open.svg b/linux/assets/file-icons/icons/light/folders/folder-scripts-open.svg deleted file mode 100644 index 519b6138c..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-scripts-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-scripts.svg b/linux/assets/file-icons/icons/light/folders/folder-scripts.svg deleted file mode 100644 index 78fa6406f..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-scripts.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-security-open.svg b/linux/assets/file-icons/icons/light/folders/folder-security-open.svg deleted file mode 100644 index 4888dcd72..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-security-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-security.svg b/linux/assets/file-icons/icons/light/folders/folder-security.svg deleted file mode 100644 index 1aa7f1769..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-security.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-source-open.svg b/linux/assets/file-icons/icons/light/folders/folder-source-open.svg deleted file mode 100644 index 1da3d2d4b..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-source-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-source.svg b/linux/assets/file-icons/icons/light/folders/folder-source.svg deleted file mode 100644 index dd71d5eb7..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-source.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-styles-open.svg b/linux/assets/file-icons/icons/light/folders/folder-styles-open.svg deleted file mode 100644 index b3bf8b742..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-styles-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-styles.svg b/linux/assets/file-icons/icons/light/folders/folder-styles.svg deleted file mode 100644 index 060ce9d00..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-styles.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-test-open.svg b/linux/assets/file-icons/icons/light/folders/folder-test-open.svg deleted file mode 100644 index 7adcf0d59..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-test-open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder-test.svg b/linux/assets/file-icons/icons/light/folders/folder-test.svg deleted file mode 100644 index 2fd97785a..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder-test.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/linux/assets/file-icons/icons/light/folders/folder.svg b/linux/assets/file-icons/icons/light/folders/folder.svg deleted file mode 100644 index a127080bf..000000000 --- a/linux/assets/file-icons/icons/light/folders/folder.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/linux/assets/file-icons/map.json b/linux/assets/file-icons/map.json index 53f5e9925..53ca02dc3 100644 --- a/linux/assets/file-icons/map.json +++ b/linux/assets/file-icons/map.json @@ -1 +1 @@ -{"defaultFile": "file", "defaultFolder": "folder", "defaultFolderOpen": "folder-open", "iconDefinitions": {"file": "./icons/files/file.svg", "text": "./icons/files/text.svg", "document": "./icons/files/document.svg", "markdown": "./icons/files/markdown.svg", "html": "./icons/files/html.svg", "css": "./icons/files/css.svg", "sass": "./icons/files/sass.svg", "javascript": "./icons/files/javascript.svg", "typescript": "./icons/files/typescript.svg", "react": "./icons/files/react.svg", "vue": "./icons/files/vue.svg", "svelte": "./icons/files/svelte.svg", "astro": "./icons/files/astro.svg", "json": "./icons/files/json.svg", "yaml": "./icons/files/yaml.svg", "toml": "./icons/files/toml.svg", "xml": "./icons/files/xml.svg", "rust": "./icons/files/rust.svg", "python": "./icons/files/python.svg", "go": "./icons/files/go.svg", "java": "./icons/files/java.svg", "c": "./icons/files/c.svg", "cpp": "./icons/files/cpp.svg", "csharp": "./icons/files/csharp.svg", "swift": "./icons/files/swift.svg", "zig": "./icons/files/zig.svg", "ruby": "./icons/files/ruby.svg", "php": "./icons/files/php.svg", "shell": "./icons/files/shell.svg", "sql": "./icons/files/sql.svg", "database": "./icons/files/database.svg", "prisma": "./icons/files/prisma.svg", "graphql": "./icons/files/graphql.svg", "docker": "./icons/files/docker.svg", "git": "./icons/files/git.svg", "github": "./icons/files/github.svg", "package": "./icons/files/package.svg", "node": "./icons/files/node.svg", "bun": "./icons/files/bun.svg", "deno": "./icons/files/deno.svg", "lock": "./icons/files/lock.svg", "config": "./icons/files/config.svg", "env": "./icons/files/env.svg", "test": "./icons/files/test.svg", "vite": "./icons/files/vite.svg", "tailwind": "./icons/files/tailwind.svg", "image": "./icons/files/image.svg", "svg": "./icons/files/svg.svg", "audio": "./icons/files/audio.svg", "video": "./icons/files/video.svg", "font": "./icons/files/font.svg", "pdf": "./icons/files/pdf.svg", "notebook": "./icons/files/notebook.svg", "next": "./icons/files/next.svg", "nuxt": "./icons/files/nuxt.svg", "angular": "./icons/files/angular.svg", "solid": "./icons/files/solid.svg", "remix": "./icons/files/remix.svg", "qwik": "./icons/files/qwik.svg", "lit": "./icons/files/lit.svg", "storybook": "./icons/files/storybook.svg", "jest": "./icons/files/jest.svg", "vitest": "./icons/files/vitest.svg", "playwright": "./icons/files/playwright.svg", "cypress": "./icons/files/cypress.svg", "eslint": "./icons/files/eslint.svg", "prettier": "./icons/files/prettier.svg", "biome": "./icons/files/biome.svg", "babel": "./icons/files/babel.svg", "swc": "./icons/files/swc.svg", "webpack": "./icons/files/webpack.svg", "rollup": "./icons/files/rollup.svg", "rspack": "./icons/files/rspack.svg", "turborepo": "./icons/files/turborepo.svg", "nx": "./icons/files/nx.svg", "npm": "./icons/files/npm.svg", "pnpm": "./icons/files/pnpm.svg", "yarn": "./icons/files/yarn.svg", "maven": "./icons/files/maven.svg", "gradle": "./icons/files/gradle.svg", "kotlin": "./icons/files/kotlin.svg", "dart": "./icons/files/dart.svg", "lua": "./icons/files/lua.svg", "elixir": "./icons/files/elixir.svg", "erlang": "./icons/files/erlang.svg", "haskell": "./icons/files/haskell.svg", "scala": "./icons/files/scala.svg", "clojure": "./icons/files/clojure.svg", "nim": "./icons/files/nim.svg", "nix": "./icons/files/nix.svg", "terraform": "./icons/files/terraform.svg", "kubernetes": "./icons/files/kubernetes.svg", "helm": "./icons/files/helm.svg", "ansible": "./icons/files/ansible.svg", "cloudflare": "./icons/files/cloudflare.svg", "netlify": "./icons/files/netlify.svg", "vercel": "./icons/files/vercel.svg", "firebase": "./icons/files/firebase.svg", "supabase": "./icons/files/supabase.svg", "mongo": "./icons/files/mongo.svg", "redis": "./icons/files/redis.svg", "postgres": "./icons/files/postgres.svg", "drizzle": "./icons/files/drizzle.svg", "figma": "./icons/files/figma.svg", "sketch": "./icons/files/sketch.svg", "adobe": "./icons/files/adobe.svg", "csv": "./icons/files/csv.svg", "spreadsheet": "./icons/files/spreadsheet.svg", "word": "./icons/files/word.svg", "powerpoint": "./icons/files/powerpoint.svg", "archive": "./icons/files/archive.svg", "certificate": "./icons/files/certificate.svg", "key": "./icons/files/key.svg", "log": "./icons/files/log.svg", "diff": "./icons/files/diff.svg", "patch": "./icons/files/patch.svg", "license": "./icons/files/license.svg", "makefile": "./icons/files/makefile.svg", "cmake": "./icons/files/cmake.svg", "proto": "./icons/files/proto.svg", "wasm": "./icons/files/wasm.svg", "rescript": "./icons/files/rescript.svg", "ocaml": "./icons/files/ocaml.svg", "solidity": "./icons/files/solidity.svg", "r": "./icons/files/r.svg", "julia": "./icons/files/julia.svg", "perl": "./icons/files/perl.svg", "lithe": "./icons/files/lithe.svg", "codex": "./icons/files/codex.svg", "claude": "./icons/files/claude.svg", "cursor": "./icons/files/cursor.svg", "tauri": "./icons/files/tauri.svg", "electron": "./icons/files/electron.svg", "xcode": "./icons/files/xcode.svg", "android": "./icons/files/android.svg", "apple": "./icons/files/apple.svg", "windows": "./icons/files/windows.svg", "linux": "./icons/files/linux.svg", "changelog": "./icons/files/changelog.svg", "authors": "./icons/files/authors.svg", "security": "./icons/files/security.svg", "warning": "./icons/files/warning.svg", "agents": "./icons/files/agents.svg", "copilot": "./icons/files/copilot.svg", "gemini": "./icons/files/gemini.svg", "cline": "./icons/files/cline.svg", "mcp": "./icons/files/mcp.svg", "editorconfig": "./icons/files/editorconfig.svg", "stylelint": "./icons/files/stylelint.svg", "markdownlint": "./icons/files/markdownlint.svg", "cspell": "./icons/files/cspell.svg", "commitlint": "./icons/files/commitlint.svg", "lintstaged": "./icons/files/lintstaged.svg", "renovate": "./icons/files/renovate.svg", "dependabot": "./icons/files/dependabot.svg", "docker-compose": "./icons/files/docker-compose.svg", "devcontainer": "./icons/files/devcontainer.svg", "github-actions": "./icons/files/github-actions.svg", "gitlab": "./icons/files/gitlab.svg", "bitbucket": "./icons/files/bitbucket.svg", "jenkins": "./icons/files/jenkins.svg", "vercel-config": "./icons/files/vercel-config.svg", "nginx": "./icons/files/nginx.svg", "http": "./icons/files/http.svg", "hurl": "./icons/files/hurl.svg", "graphql-schema": "./icons/files/graphql-schema.svg", "jsconfig": "./icons/files/jsconfig.svg", "index-js": "./icons/files/index-js.svg", "index-ts": "./icons/files/index-ts.svg", "layout": "./icons/files/layout.svg", "page": "./icons/files/page.svg", "route": "./icons/files/route.svg", "loading": "./icons/files/loading.svg", "not-found": "./icons/files/not-found.svg", "error": "./icons/files/error.svg", "docusaurus": "./icons/files/docusaurus.svg", "gatsby": "./icons/files/gatsby.svg", "laravel": "./icons/files/laravel.svg", "django": "./icons/files/django.svg", "flask": "./icons/files/flask.svg", "fastapi": "./icons/files/fastapi.svg", "arduino": "./icons/files/arduino.svg", "blender": "./icons/files/blender.svg", "drawio": "./icons/files/drawio.svg", "excalidraw": "./icons/files/excalidraw.svg", "mermaid": "./icons/files/mermaid.svg", "folder": "./icons/folders/folder.svg", "folder-open": "./icons/folders/folder-open.svg", "folder-source": "./icons/folders/folder-source.svg", "folder-source-open": "./icons/folders/folder-source-open.svg", "folder-components": "./icons/folders/folder-components.svg", "folder-components-open": "./icons/folders/folder-components-open.svg", "folder-test": "./icons/folders/folder-test.svg", "folder-test-open": "./icons/folders/folder-test-open.svg", "folder-config": "./icons/folders/folder-config.svg", "folder-config-open": "./icons/folders/folder-config-open.svg", "folder-assets": "./icons/folders/folder-assets.svg", "folder-assets-open": "./icons/folders/folder-assets-open.svg", "folder-docs": "./icons/folders/folder-docs.svg", "folder-docs-open": "./icons/folders/folder-docs-open.svg", "folder-scripts": "./icons/folders/folder-scripts.svg", "folder-scripts-open": "./icons/folders/folder-scripts-open.svg", "folder-rust": "./icons/folders/folder-rust.svg", "folder-rust-open": "./icons/folders/folder-rust-open.svg", "folder-packages": "./icons/folders/folder-packages.svg", "folder-packages-open": "./icons/folders/folder-packages-open.svg", "folder-git": "./icons/folders/folder-git.svg", "folder-git-open": "./icons/folders/folder-git-open.svg", "folder-build": "./icons/folders/folder-build.svg", "folder-build-open": "./icons/folders/folder-build-open.svg", "folder-database": "./icons/folders/folder-database.svg", "folder-database-open": "./icons/folders/folder-database-open.svg", "folder-routes": "./icons/folders/folder-routes.svg", "folder-routes-open": "./icons/folders/folder-routes-open.svg", "folder-styles": "./icons/folders/folder-styles.svg", "folder-styles-open": "./icons/folders/folder-styles-open.svg", "folder-locales": "./icons/folders/folder-locales.svg", "folder-locales-open": "./icons/folders/folder-locales-open.svg", "folder-cloud": "./icons/folders/folder-cloud.svg", "folder-cloud-open": "./icons/folders/folder-cloud-open.svg", "folder-mobile": "./icons/folders/folder-mobile.svg", "folder-mobile-open": "./icons/folders/folder-mobile-open.svg", "folder-security": "./icons/folders/folder-security.svg", "folder-security-open": "./icons/folders/folder-security-open.svg", "folder-ai": "./icons/folders/folder-ai.svg", "folder-ai-open": "./icons/folders/folder-ai-open.svg", "folder-extensions": "./icons/folders/folder-extensions.svg", "folder-extensions-open": "./icons/folders/folder-extensions-open.svg"}, "lightIconDefinitions": {"file": "./icons/light/files/file.svg", "text": "./icons/light/files/text.svg", "document": "./icons/light/files/document.svg", "markdown": "./icons/light/files/markdown.svg", "html": "./icons/light/files/html.svg", "css": "./icons/light/files/css.svg", "sass": "./icons/light/files/sass.svg", "javascript": "./icons/light/files/javascript.svg", "typescript": "./icons/light/files/typescript.svg", "react": "./icons/light/files/react.svg", "vue": "./icons/light/files/vue.svg", "svelte": "./icons/light/files/svelte.svg", "astro": "./icons/light/files/astro.svg", "json": "./icons/light/files/json.svg", "yaml": "./icons/light/files/yaml.svg", "toml": "./icons/light/files/toml.svg", "xml": "./icons/light/files/xml.svg", "rust": "./icons/light/files/rust.svg", "python": "./icons/light/files/python.svg", "go": "./icons/light/files/go.svg", "java": "./icons/light/files/java.svg", "c": "./icons/light/files/c.svg", "cpp": "./icons/light/files/cpp.svg", "csharp": "./icons/light/files/csharp.svg", "swift": "./icons/light/files/swift.svg", "zig": "./icons/light/files/zig.svg", "ruby": "./icons/light/files/ruby.svg", "php": "./icons/light/files/php.svg", "shell": "./icons/light/files/shell.svg", "sql": "./icons/light/files/sql.svg", "database": "./icons/light/files/database.svg", "prisma": "./icons/light/files/prisma.svg", "graphql": "./icons/light/files/graphql.svg", "docker": "./icons/light/files/docker.svg", "git": "./icons/light/files/git.svg", "github": "./icons/light/files/github.svg", "package": "./icons/light/files/package.svg", "node": "./icons/light/files/node.svg", "bun": "./icons/light/files/bun.svg", "deno": "./icons/light/files/deno.svg", "lock": "./icons/light/files/lock.svg", "config": "./icons/light/files/config.svg", "env": "./icons/light/files/env.svg", "test": "./icons/light/files/test.svg", "vite": "./icons/light/files/vite.svg", "tailwind": "./icons/light/files/tailwind.svg", "image": "./icons/light/files/image.svg", "svg": "./icons/light/files/svg.svg", "audio": "./icons/light/files/audio.svg", "video": "./icons/light/files/video.svg", "font": "./icons/light/files/font.svg", "pdf": "./icons/light/files/pdf.svg", "notebook": "./icons/light/files/notebook.svg", "next": "./icons/light/files/next.svg", "nuxt": "./icons/light/files/nuxt.svg", "angular": "./icons/light/files/angular.svg", "solid": "./icons/light/files/solid.svg", "remix": "./icons/light/files/remix.svg", "qwik": "./icons/light/files/qwik.svg", "lit": "./icons/light/files/lit.svg", "storybook": "./icons/light/files/storybook.svg", "jest": "./icons/light/files/jest.svg", "vitest": "./icons/light/files/vitest.svg", "playwright": "./icons/light/files/playwright.svg", "cypress": "./icons/light/files/cypress.svg", "eslint": "./icons/light/files/eslint.svg", "prettier": "./icons/light/files/prettier.svg", "biome": "./icons/light/files/biome.svg", "babel": "./icons/light/files/babel.svg", "swc": "./icons/light/files/swc.svg", "webpack": "./icons/light/files/webpack.svg", "rollup": "./icons/light/files/rollup.svg", "rspack": "./icons/light/files/rspack.svg", "turborepo": "./icons/light/files/turborepo.svg", "nx": "./icons/light/files/nx.svg", "npm": "./icons/light/files/npm.svg", "pnpm": "./icons/light/files/pnpm.svg", "yarn": "./icons/light/files/yarn.svg", "maven": "./icons/light/files/maven.svg", "gradle": "./icons/light/files/gradle.svg", "kotlin": "./icons/light/files/kotlin.svg", "dart": "./icons/light/files/dart.svg", "lua": "./icons/light/files/lua.svg", "elixir": "./icons/light/files/elixir.svg", "erlang": "./icons/light/files/erlang.svg", "haskell": "./icons/light/files/haskell.svg", "scala": "./icons/light/files/scala.svg", "clojure": "./icons/light/files/clojure.svg", "nim": "./icons/light/files/nim.svg", "nix": "./icons/light/files/nix.svg", "terraform": "./icons/light/files/terraform.svg", "kubernetes": "./icons/light/files/kubernetes.svg", "helm": "./icons/light/files/helm.svg", "ansible": "./icons/light/files/ansible.svg", "cloudflare": "./icons/light/files/cloudflare.svg", "netlify": "./icons/light/files/netlify.svg", "vercel": "./icons/light/files/vercel.svg", "firebase": "./icons/light/files/firebase.svg", "supabase": "./icons/light/files/supabase.svg", "mongo": "./icons/light/files/mongo.svg", "redis": "./icons/light/files/redis.svg", "postgres": "./icons/light/files/postgres.svg", "drizzle": "./icons/light/files/drizzle.svg", "figma": "./icons/light/files/figma.svg", "sketch": "./icons/light/files/sketch.svg", "adobe": "./icons/light/files/adobe.svg", "csv": "./icons/light/files/csv.svg", "spreadsheet": "./icons/light/files/spreadsheet.svg", "word": "./icons/light/files/word.svg", "powerpoint": "./icons/light/files/powerpoint.svg", "archive": "./icons/light/files/archive.svg", "certificate": "./icons/light/files/certificate.svg", "key": "./icons/light/files/key.svg", "log": "./icons/light/files/log.svg", "diff": "./icons/light/files/diff.svg", "patch": "./icons/light/files/patch.svg", "license": "./icons/light/files/license.svg", "makefile": "./icons/light/files/makefile.svg", "cmake": "./icons/light/files/cmake.svg", "proto": "./icons/light/files/proto.svg", "wasm": "./icons/light/files/wasm.svg", "rescript": "./icons/light/files/rescript.svg", "ocaml": "./icons/light/files/ocaml.svg", "solidity": "./icons/light/files/solidity.svg", "r": "./icons/light/files/r.svg", "julia": "./icons/light/files/julia.svg", "perl": "./icons/light/files/perl.svg", "lithe": "./icons/light/files/lithe.svg", "codex": "./icons/light/files/codex.svg", "claude": "./icons/light/files/claude.svg", "cursor": "./icons/light/files/cursor.svg", "tauri": "./icons/light/files/tauri.svg", "electron": "./icons/light/files/electron.svg", "xcode": "./icons/light/files/xcode.svg", "android": "./icons/light/files/android.svg", "apple": "./icons/light/files/apple.svg", "windows": "./icons/light/files/windows.svg", "linux": "./icons/light/files/linux.svg", "changelog": "./icons/light/files/changelog.svg", "authors": "./icons/light/files/authors.svg", "security": "./icons/light/files/security.svg", "warning": "./icons/light/files/warning.svg", "agents": "./icons/light/files/agents.svg", "copilot": "./icons/light/files/copilot.svg", "gemini": "./icons/light/files/gemini.svg", "cline": "./icons/light/files/cline.svg", "mcp": "./icons/light/files/mcp.svg", "editorconfig": "./icons/light/files/editorconfig.svg", "stylelint": "./icons/light/files/stylelint.svg", "markdownlint": "./icons/light/files/markdownlint.svg", "cspell": "./icons/light/files/cspell.svg", "commitlint": "./icons/light/files/commitlint.svg", "lintstaged": "./icons/light/files/lintstaged.svg", "renovate": "./icons/light/files/renovate.svg", "dependabot": "./icons/light/files/dependabot.svg", "docker-compose": "./icons/light/files/docker-compose.svg", "devcontainer": "./icons/light/files/devcontainer.svg", "github-actions": "./icons/light/files/github-actions.svg", "gitlab": "./icons/light/files/gitlab.svg", "bitbucket": "./icons/light/files/bitbucket.svg", "jenkins": "./icons/light/files/jenkins.svg", "vercel-config": "./icons/light/files/vercel-config.svg", "nginx": "./icons/light/files/nginx.svg", "http": "./icons/light/files/http.svg", "hurl": "./icons/light/files/hurl.svg", "graphql-schema": "./icons/light/files/graphql-schema.svg", "jsconfig": "./icons/light/files/jsconfig.svg", "index-js": "./icons/light/files/index-js.svg", "index-ts": "./icons/light/files/index-ts.svg", "layout": "./icons/light/files/layout.svg", "page": "./icons/light/files/page.svg", "route": "./icons/light/files/route.svg", "loading": "./icons/light/files/loading.svg", "not-found": "./icons/light/files/not-found.svg", "error": "./icons/light/files/error.svg", "docusaurus": "./icons/light/files/docusaurus.svg", "gatsby": "./icons/light/files/gatsby.svg", "laravel": "./icons/light/files/laravel.svg", "django": "./icons/light/files/django.svg", "flask": "./icons/light/files/flask.svg", "fastapi": "./icons/light/files/fastapi.svg", "arduino": "./icons/light/files/arduino.svg", "blender": "./icons/light/files/blender.svg", "drawio": "./icons/light/files/drawio.svg", "excalidraw": "./icons/light/files/excalidraw.svg", "mermaid": "./icons/light/files/mermaid.svg", "folder": "./icons/light/folders/folder.svg", "folder-open": "./icons/light/folders/folder-open.svg", "folder-source": "./icons/light/folders/folder-source.svg", "folder-source-open": "./icons/light/folders/folder-source-open.svg", "folder-components": "./icons/light/folders/folder-components.svg", "folder-components-open": "./icons/light/folders/folder-components-open.svg", "folder-test": "./icons/light/folders/folder-test.svg", "folder-test-open": "./icons/light/folders/folder-test-open.svg", "folder-config": "./icons/light/folders/folder-config.svg", "folder-config-open": "./icons/light/folders/folder-config-open.svg", "folder-assets": "./icons/light/folders/folder-assets.svg", "folder-assets-open": "./icons/light/folders/folder-assets-open.svg", "folder-docs": "./icons/light/folders/folder-docs.svg", "folder-docs-open": "./icons/light/folders/folder-docs-open.svg", "folder-scripts": "./icons/light/folders/folder-scripts.svg", "folder-scripts-open": "./icons/light/folders/folder-scripts-open.svg", "folder-rust": "./icons/light/folders/folder-rust.svg", "folder-rust-open": "./icons/light/folders/folder-rust-open.svg", "folder-packages": "./icons/light/folders/folder-packages.svg", "folder-packages-open": "./icons/light/folders/folder-packages-open.svg", "folder-git": "./icons/light/folders/folder-git.svg", "folder-git-open": "./icons/light/folders/folder-git-open.svg", "folder-build": "./icons/light/folders/folder-build.svg", "folder-build-open": "./icons/light/folders/folder-build-open.svg", "folder-database": "./icons/light/folders/folder-database.svg", "folder-database-open": "./icons/light/folders/folder-database-open.svg", "folder-routes": "./icons/light/folders/folder-routes.svg", "folder-routes-open": "./icons/light/folders/folder-routes-open.svg", "folder-styles": "./icons/light/folders/folder-styles.svg", "folder-styles-open": "./icons/light/folders/folder-styles-open.svg", "folder-locales": "./icons/light/folders/folder-locales.svg", "folder-locales-open": "./icons/light/folders/folder-locales-open.svg", "folder-cloud": "./icons/light/folders/folder-cloud.svg", "folder-cloud-open": "./icons/light/folders/folder-cloud-open.svg", "folder-mobile": "./icons/light/folders/folder-mobile.svg", "folder-mobile-open": "./icons/light/folders/folder-mobile-open.svg", "folder-security": "./icons/light/folders/folder-security.svg", "folder-security-open": "./icons/light/folders/folder-security-open.svg", "folder-ai": "./icons/light/folders/folder-ai.svg", "folder-ai-open": "./icons/light/folders/folder-ai-open.svg", "folder-extensions": "./icons/light/folders/folder-extensions.svg", "folder-extensions-open": "./icons/light/folders/folder-extensions-open.svg"}, "fileExtensions": {".txt": "text", ".md": "markdown", ".mdx": "markdown", ".html": "html", ".htm": "html", ".css": "css", ".scss": "sass", ".sass": "sass", ".js": "javascript", ".mjs": "javascript", ".cjs": "javascript", ".jsx": "react", ".ts": "typescript", ".mts": "typescript", ".cts": "typescript", ".tsx": "react", ".vue": "vue", ".svelte": "svelte", ".astro": "astro", ".json": "json", ".jsonc": "json", ".yaml": "yaml", ".yml": "yaml", ".toml": "toml", ".xml": "xml", ".svg": "svg", ".rs": "rust", ".ron": "rust", ".py": "python", ".go": "go", ".java": "java", ".c": "c", ".h": "c", ".cpp": "cpp", ".cxx": "cpp", ".cc": "cpp", ".hpp": "cpp", ".cs": "csharp", ".swift": "swift", ".zig": "zig", ".rb": "ruby", ".php": "php", ".sh": "shell", ".bash": "shell", ".zsh": "shell", ".fish": "shell", ".sql": "sql", ".sqlite": "database", ".sqlite3": "database", ".db": "database", ".prisma": "prisma", ".graphql": "graphql", ".gql": "graphql", ".dockerfile": "docker", ".lock": "lock", ".env": "env", ".test.js": "test", ".test.ts": "test", ".test.tsx": "test", ".spec.js": "test", ".spec.ts": "test", ".spec.tsx": "test", ".png": "image", ".jpg": "image", ".jpeg": "image", ".webp": "image", ".gif": "image", ".ico": "image", ".mp3": "audio", ".wav": "audio", ".flac": "audio", ".mp4": "video", ".mov": "video", ".webm": "video", ".ttf": "font", ".otf": "font", ".woff": "font", ".woff2": "font", ".pdf": "pdf", ".ipynb": "notebook", ".vue.ts": "vue", ".svelte.ts": "svelte", ".stories.js": "storybook", ".stories.jsx": "storybook", ".stories.ts": "storybook", ".stories.tsx": "storybook", ".story.js": "storybook", ".story.jsx": "storybook", ".story.ts": "storybook", ".story.tsx": "storybook", ".cy.js": "cypress", ".cy.ts": "cypress", ".cy.tsx": "cypress", ".playwright.js": "playwright", ".playwright.ts": "playwright", ".test.mjs": "test", ".spec.mjs": "test", ".kt": "kotlin", ".kts": "kotlin", ".dart": "dart", ".lua": "lua", ".ex": "elixir", ".exs": "elixir", ".erl": "erlang", ".hrl": "erlang", ".hs": "haskell", ".scala": "scala", ".sc": "scala", ".clj": "clojure", ".cljs": "clojure", ".nim": "nim", ".nix": "nix", ".tf": "terraform", ".tfvars": "terraform", ".hcl": "terraform", ".k8s.yaml": "kubernetes", ".helm.yaml": "helm", ".yarnrc": "yarn", ".npmrc": "npm", ".csv": "csv", ".tsv": "csv", ".xlsx": "spreadsheet", ".xls": "spreadsheet", ".doc": "word", ".docx": "word", ".ppt": "powerpoint", ".pptx": "powerpoint", ".zip": "archive", ".tar": "archive", ".gz": "archive", ".tgz": "archive", ".rar": "archive", ".7z": "archive", ".pem": "certificate", ".crt": "certificate", ".cer": "certificate", ".key": "key", ".log": "log", ".diff": "diff", ".patch": "patch", ".proto": "proto", ".wasm": "wasm", ".res": "rescript", ".resi": "rescript", ".ml": "ocaml", ".mli": "ocaml", ".sol": "solidity", ".r": "r", ".rmd": "r", ".jl": "julia", ".pl": "perl", ".app": "apple", ".ipa": "apple", ".apk": "android", ".aab": "android", ".exe": "windows", ".msi": "windows", ".dll": "windows", ".so": "linux", ".http": "http", ".rest": "http", ".hurl": "hurl", ".drawio": "drawio", ".excalidraw": "excalidraw", ".mmd": "mermaid", ".mermaid": "mermaid", ".blend": "blender", ".ino": "arduino"}, "filenames": {"package.json": "node", "package-lock.json": "node", "bun.lock": "bun", "bun.lockb": "bun", "bunfig.toml": "bun", "deno.json": "deno", "deno.jsonc": "deno", "tsconfig.json": "typescript", "vite.config.js": "vite", "vite.config.ts": "vite", "tailwind.config.js": "tailwind", "tailwind.config.ts": "tailwind", "dockerfile": "docker", ".dockerignore": "docker", ".gitignore": "git", ".gitattributes": "git", ".env": "env", ".env.example": "env", "cargo.toml": "rust", "cargo.lock": "rust", "readme.md": "markdown", "license": "license", "license.md": "license", "next.config.js": "next", "next.config.mjs": "next", "next.config.ts": "next", "nuxt.config.js": "nuxt", "nuxt.config.ts": "nuxt", "angular.json": "angular", "remix.config.js": "remix", "remix.config.ts": "remix", "qwik.config.js": "qwik", "qwik.config.ts": "qwik", "lit.config.js": "lit", "storybook.config.js": "storybook", "jest.config.js": "jest", "jest.config.ts": "jest", "vitest.config.js": "vitest", "vitest.config.ts": "vitest", "playwright.config.js": "playwright", "playwright.config.ts": "playwright", "cypress.config.js": "cypress", "cypress.config.ts": "cypress", ".eslintrc": "eslint", ".eslintrc.json": "eslint", "eslint.config.js": "eslint", "eslint.config.mjs": "eslint", "eslint.config.ts": "eslint", ".prettierrc": "prettier", ".prettierrc.json": "prettier", "prettier.config.js": "prettier", "biome.json": "biome", "biome.jsonc": "biome", "babel.config.js": "babel", ".babelrc": "babel", ".swcrc": "swc", "webpack.config.js": "webpack", "webpack.config.ts": "webpack", "rollup.config.js": "rollup", "rollup.config.ts": "rollup", "rspack.config.js": "rspack", "rspack.config.ts": "rspack", "turbo.json": "turborepo", "nx.json": "nx", "pnpm-lock.yaml": "pnpm", "pnpm-workspace.yaml": "pnpm", "yarn.lock": "yarn", "pom.xml": "maven", "build.gradle": "gradle", "build.gradle.kts": "gradle", "gradle.properties": "gradle", "gradlew": "gradle", "flake.nix": "nix", "terraform.tfvars": "terraform", "chart.yaml": "helm", "ansible.cfg": "ansible", "wrangler.toml": "cloudflare", "netlify.toml": "netlify", "vercel.json": "vercel", "firebase.json": "firebase", "supabase.toml": "supabase", "drizzle.config.ts": "drizzle", ".fig": "figma", "makefile": "makefile", "cmakelists.txt": "cmake", "copying": "license", "lithe.json": "lithe", ".codex": "codex", "agents.md": "codex", "claude.md": "claude", ".cursorrules": "cursor", "tauri.conf.json": "tauri", "electron-builder.json": "electron", "xcodeproj": "xcode", "androidmanifest.xml": "android", "changelog.md": "changelog", "authors": "authors", "authors.md": "authors", "security.md": "security", "codeowners": "security", ".agents": "agents", "agents.json": "agents", "agents.toml": "agents", "copilot-instructions.md": "copilot", ".mcp.json": "mcp", "mcp.json": "mcp", ".editorconfig": "editorconfig", ".stylelintrc": "stylelint", ".stylelintrc.json": "stylelint", "stylelint.config.js": "stylelint", "stylelint.config.mjs": "stylelint", ".markdownlint.json": "markdownlint", ".markdownlint.yaml": "markdownlint", ".markdownlintignore": "markdownlint", "cspell.json": "cspell", ".cspell.json": "cspell", "commitlint.config.js": "commitlint", "commitlint.config.ts": "commitlint", ".lintstagedrc": "lintstaged", "lint-staged.config.js": "lintstaged", "renovate.json": "renovate", "dependabot.yml": "dependabot", "docker-compose.yml": "docker-compose", "docker-compose.yaml": "docker-compose", ".devcontainer.json": "devcontainer", "devcontainer.json": "devcontainer", "action.yml": "github-actions", "action.yaml": "github-actions", ".gitlab-ci.yml": "gitlab", "bitbucket-pipelines.yml": "bitbucket", "jenkinsfile": "jenkins", "nginx.conf": "nginx", "index.js": "index-js", "index.ts": "index-ts", "index.tsx": "index-ts", "layout.js": "layout", "layout.jsx": "layout", "layout.ts": "layout", "layout.tsx": "layout", "page.js": "page", "page.jsx": "page", "page.ts": "page", "page.tsx": "page", "route.js": "route", "route.ts": "route", "loading.js": "loading", "loading.tsx": "loading", "not-found.js": "not-found", "not-found.tsx": "not-found", "error.js": "error", "error.tsx": "error", "docusaurus.config.js": "docusaurus", "docusaurus.config.ts": "docusaurus", "gatsby-config.js": "gatsby", "gatsby-node.js": "gatsby", "artisan": "laravel", "manage.py": "django", "app.py": "flask", "main.py": "python", "requirements.txt": "python"}, "folders": {"src": "folder-source", "source": "folder-source", "components": "folder-components", "component": "folder-components", "hooks": "folder-source", "utils": "folder-source", "util": "folder-source", "tests": "folder-test", "test": "folder-test", "__tests__": "folder-test", "config": "folder-config", "configs": "folder-config", "assets": "folder-assets", "public": "folder-assets", "images": "folder-assets", "img": "folder-assets", "docs": "folder-docs", "scripts": "folder-scripts", "crates": "folder-rust", "rust": "folder-rust", "src-tauri": "folder-rust", "tauri": "folder-rust", "node_modules": "folder-packages", ".git": "folder-git", ".github": "folder-git", ".vscode": "folder-config", "build": "folder-build", "dist": "folder-build", "database": "folder-database", "databases": "folder-database", "api": "folder-routes", "routes": "folder-routes", "router": "folder-routes", "stores": "folder-source", "store": "folder-source", "features": "folder-source", "ui": "folder-components", "pages": "folder-components", "app": "folder-components", "views": "folder-components", "view": "folder-components", "lib": "folder-source", "libs": "folder-source", "services": "folder-source", "service": "folder-source", "types": "folder-source", "typings": "folder-source", "styles": "folder-styles", "style": "folder-styles", "css": "folder-styles", "locales": "folder-locales", "i18n": "folder-locales", "terminal": "folder-scripts", "shell": "folder-scripts", "auth": "folder-security", "security": "folder-security", ".circleci": "folder-git", ".buildkite": "folder-git", ".docker": "folder-cloud", "docker": "folder-cloud", "k8s": "folder-cloud", "kubernetes": "folder-cloud", "helm": "folder-cloud", "cloud": "folder-cloud", ".firebase": "folder-database", "firebase": "folder-database", ".supabase": "folder-database", "supabase": "folder-database", "prisma": "folder-database", "cache": "folder-build", "logs": "folder-build", "log": "folder-build", "tmp": "folder-build", "temp": "folder-build", "mobile": "folder-mobile", "ios": "folder-mobile", "android": "folder-mobile", ".storybook": "folder-components", "storybook": "folder-components", "fixtures": "folder-test", "mocks": "folder-test", "mock": "folder-test", "generated": "folder-build", "gen": "folder-build", "benchmark": "folder-test", "benchmarks": "folder-test", "extensions": "folder-extensions", "extension": "folder-extensions", "themes": "folder-styles", "theme": "folder-styles", "ai": "folder-ai", ".agents": "folder-ai", "agents": "folder-ai", ".codex": "folder-ai", "codex": "folder-ai", ".claude": "folder-ai", "claude": "folder-ai", "packages": "folder-packages", "package": "folder-packages", "examples": "folder-docs", "example": "folder-docs", "playground": "folder-test", "play": "folder-test", "commands": "folder-scripts", "command": "folder-scripts", "plugins": "folder-extensions", "plugin": "folder-extensions", "workflows": "folder-git", "workflow": "folder-git", ".devcontainer": "folder-cloud", "devcontainer": "folder-cloud", "web": "folder-mobile", "www": "folder-mobile", "server": "folder-routes", "client": "folder-mobile", "shared": "folder-source", "common": "folder-source"}, "expandedFolders": {"src": "folder-source-open", "source": "folder-source-open", "components": "folder-components-open", "component": "folder-components-open", "hooks": "folder-source-open", "utils": "folder-source-open", "util": "folder-source-open", "tests": "folder-test-open", "test": "folder-test-open", "__tests__": "folder-test-open", "config": "folder-config-open", "configs": "folder-config-open", "assets": "folder-assets-open", "public": "folder-assets-open", "images": "folder-assets-open", "img": "folder-assets-open", "docs": "folder-docs-open", "scripts": "folder-scripts-open", "crates": "folder-rust-open", "rust": "folder-rust-open", "src-tauri": "folder-rust-open", "tauri": "folder-rust-open", "node_modules": "folder-packages-open", ".git": "folder-git-open", ".github": "folder-git-open", ".vscode": "folder-config-open", "build": "folder-build-open", "dist": "folder-build-open", "database": "folder-database-open", "databases": "folder-database-open", "api": "folder-routes-open", "routes": "folder-routes-open", "router": "folder-routes-open", "stores": "folder-source-open", "store": "folder-source-open", "features": "folder-source-open", "ui": "folder-components-open", "pages": "folder-components-open", "app": "folder-components-open", "views": "folder-components-open", "view": "folder-components-open", "lib": "folder-source-open", "libs": "folder-source-open", "services": "folder-source-open", "service": "folder-source-open", "types": "folder-source-open", "typings": "folder-source-open", "styles": "folder-styles-open", "style": "folder-styles-open", "css": "folder-styles-open", "locales": "folder-locales-open", "i18n": "folder-locales-open", "terminal": "folder-scripts-open", "shell": "folder-scripts-open", "auth": "folder-security-open", "security": "folder-security-open", ".circleci": "folder-git-open", ".buildkite": "folder-git-open", ".docker": "folder-cloud-open", "docker": "folder-cloud-open", "k8s": "folder-cloud-open", "kubernetes": "folder-cloud-open", "helm": "folder-cloud-open", "cloud": "folder-cloud-open", ".firebase": "folder-database-open", "firebase": "folder-database-open", ".supabase": "folder-database-open", "supabase": "folder-database-open", "prisma": "folder-database-open", "cache": "folder-build-open", "logs": "folder-build-open", "log": "folder-build-open", "tmp": "folder-build-open", "temp": "folder-build-open", "mobile": "folder-mobile-open", "ios": "folder-mobile-open", "android": "folder-mobile-open", ".storybook": "folder-components-open", "storybook": "folder-components-open", "fixtures": "folder-test-open", "mocks": "folder-test-open", "mock": "folder-test-open", "generated": "folder-build-open", "gen": "folder-build-open", "benchmark": "folder-test-open", "benchmarks": "folder-test-open", "extensions": "folder-extensions-open", "extension": "folder-extensions-open", "themes": "folder-styles-open", "theme": "folder-styles-open", "ai": "folder-ai-open", ".agents": "folder-ai-open", "agents": "folder-ai-open", ".codex": "folder-ai-open", "codex": "folder-ai-open", ".claude": "folder-ai-open", "claude": "folder-ai-open", "packages": "folder-packages-open", "package": "folder-packages-open", "examples": "folder-docs-open", "example": "folder-docs-open", "playground": "folder-test-open", "play": "folder-test-open", "commands": "folder-scripts-open", "command": "folder-scripts-open", "plugins": "folder-extensions-open", "plugin": "folder-extensions-open", "workflows": "folder-git-open", "workflow": "folder-git-open", ".devcontainer": "folder-cloud-open", "devcontainer": "folder-cloud-open", "web": "folder-mobile-open", "www": "folder-mobile-open", "server": "folder-routes-open", "client": "folder-mobile-open", "shared": "folder-source-open", "common": "folder-source-open"}} \ No newline at end of file +{"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/src/workbench/editor.rs b/linux/src/workbench/editor.rs index a8c31fab7..d2fcecd80 100644 --- a/linux/src/workbench/editor.rs +++ b/linux/src/workbench/editor.rs @@ -167,6 +167,13 @@ impl EditorView { }; 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); From 58662f3f25130a6342d09353918318f010864408 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Thu, 24 Sep 2026 23:57:02 +0800 Subject: [PATCH 29/59] =?UTF-8?q?chore(core):=20tree-sitter=200.25=20?= =?UTF-8?q?=E5=8D=87=200.26=EF=BC=8C=E4=B8=8E=20gpui-kit=20=E9=AB=98?= =?UTF-8?q?=E4=BA=AE=E7=89=B9=E6=80=A7=E5=AF=B9=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - links 冲突:全图只允许一份 tree-sitter 原生库 - core 仅用 Parser/Node 基础 API,适配 Node::child 改取 u32 --- linux/Cargo.lock | 109 ++++++++++++++++++++++- rust/lithe-core/Cargo.toml | 2 +- rust/lithe-core/src/languages/mybatis.rs | 3 +- 3 files changed, 108 insertions(+), 6 deletions(-) diff --git a/linux/Cargo.lock b/linux/Cargo.lock index c7ef06208..600338172 100644 --- a/linux/Cargo.lock +++ b/linux/Cargo.lock @@ -880,9 +880,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.7" +version = "1.2.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" +checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" dependencies = [ "find-msvc-tools", "jobserver", @@ -2464,6 +2464,17 @@ dependencies = [ "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", ] @@ -7638,9 +7649,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.25.10" +version = "0.26.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87" +checksum = "17ebdd3a5a7e28a1890b876fdbd0c3c0fe0a6336cffaa104f11b9f720c9daa29" dependencies = [ "cc", "regex", @@ -7650,6 +7661,16 @@ dependencies = [ "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" @@ -7660,12 +7681,92 @@ dependencies = [ "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" diff --git a/rust/lithe-core/Cargo.toml b/rust/lithe-core/Cargo.toml index 53800e763..68f7618d9 100644 --- a/rust/lithe-core/Cargo.toml +++ b/rust/lithe-core/Cargo.toml @@ -23,7 +23,7 @@ serde_json = "1.0" sha1 = "0.10" sha2 = "0.10" toml = { version = "1.1.4", default-features = false, features = ["parse", "serde"] } -tree-sitter = "0.25" +tree-sitter = "0.26" tree-sitter-java = "0.23" serde_yaml_ng = "0.10.0" zip = { version = "2.4", default-features = false, features = ["deflate"] } diff --git a/rust/lithe-core/src/languages/mybatis.rs b/rust/lithe-core/src/languages/mybatis.rs index 2bd9fb63f..b8aeeaaba 100644 --- a/rust/lithe-core/src/languages/mybatis.rs +++ b/rust/lithe-core/src/languages/mybatis.rs @@ -216,8 +216,9 @@ fn collect_types( } } // Reverse the push order to preserve the recursive walk's source preorder. + // tree-sitter 0.26 起 `Node::child` 取 `u32`,`child_count` 仍为 `usize`。 for index in (0..node.child_count()).rev() { - if let Some(child) = node.child(index) { + if let Some(child) = node.child(index as u32) { pending.push(child); } } From 2bcdaf3e2b2c9989bd570c1683991421eaec439b Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 00:01:09 +0800 Subject: [PATCH 30/59] =?UTF-8?q?feat(linux):=20Run=20=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?=E7=9C=9F=E5=AE=9E=E6=89=A7=E8=A1=8C=E9=93=BE=E4=B8=8E=20Maven/?= =?UTF-8?q?LSP=20=E6=8E=A5=E7=BA=BF=EF=BC=8C=E5=AF=B9=E9=BD=90=20Tauri?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 底部 Run Tab:maven.scan 探工程 + runConfig.generate 取配置 + createLaunchPlan 执行,子进程输出流式展示,可停止;无 LSP 时文本扫描 main 方法兜底 - 运行统一经 send_terminal_command 计入历史并点亮 maven 项;run.start 补命令映射 - Maven 面板:core 扫描状态、依赖展开、自定义 goal、Reload - LSP 分组:语言服务器探测状态诚实展示(本机无 jdtls,不伪造启动) - 工作区切换经 set_working_dir 触发 Run 重探 --- linux/src/workbench/bottom_panel.rs | 1037 +++++++++++++++++++++++- linux/src/workbench/maven.rs | 563 +++++++++++-- linux/src/workbench/settings_dialog.rs | 80 +- linux/src/workbench/view.rs | 4 +- 4 files changed, 1579 insertions(+), 105 deletions(-) diff --git a/linux/src/workbench/bottom_panel.rs b/linux/src/workbench/bottom_panel.rs index 1109dc8a7..d24063ac0 100644 --- a/linux/src/workbench/bottom_panel.rs +++ b/linux/src/workbench/bottom_panel.rs @@ -1,19 +1,64 @@ +use std::sync::{mpsc, Arc, Mutex}; + 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::component::{h_flex, v_flex, Disableable as _, Icon, Sizable as _}; use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::{ div, px, AnyElement, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement, ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Window, }; +use crate::core::CoreClient; use crate::theme::ThemeColors; use crate::workbench::terminal::TerminalView; /// 运行历史上限。 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), +} + +/// Run 配置行:本地 `.lithe/run/configurations.json` 与 +/// `runConfig.generate` 结果的显示子集。 +#[derive(Debug, Clone)] +pub struct RunConfigItem { + /// core 配置 id(本地文件项用配置名代替;`createLaunchPlan` + /// 可能因此找不到而走退化执行)。 + pub id: String, + pub name: String, + /// `mainClass`(java)|`maven`|`npm`。 + pub kind: String, + /// 副标题:主类 / 模块 / 脚本参数。 + pub detail: String, + pub main_class: Option, + /// java 源码路径(generate 结果的 `extensions.java.source`),单文件退化编译用。 + pub source: Option, +} + +/// 后台运行步骤(退化执行用,不经 core 常驻进程宿主)。 +#[derive(Debug, Clone)] +struct RunStep { + program: String, + args: Vec, + cwd: String, +} + /// Git 提交记录行数上限。 const MAX_GIT_LOG_ENTRIES: usize = 50; @@ -43,6 +88,24 @@ pub struct BottomPanelView { /// 运行历史(`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 配置列表(本地文件优先,否则 generate 结果)。 + pub(crate) run_configs: Vec, + /// 选中的 Run 配置 id。 + pub(crate) selected_run_config: Option, + /// 运行输出行(后台线程经 channel 推送,`cx.spawn` 泵入)。 + pub(crate) run_output: Vec, + /// 是否有进程在跑。 + pub(crate) run_running: bool, + /// core 客户端(`reload_run_project` / `createLaunchPlan` 经它走 core JSON 命令)。 + client: CoreClient, + /// 在跑子进程句柄:停止按钮经 `kill` 停,运行线程经 `try_wait` 短锁轮询收割。 + run_child: Arc>>, + /// reload 序号,丢弃过期探测结果。 + run_seq: u64, /// 最近一次加载的 Git 提交记录。 pub git_log: Vec, /// Git 记录加载失败时的展示文案;成功后清空。 @@ -59,8 +122,17 @@ impl BottomPanelView { height: 240.0, terminal, diagnostics: Vec::new(), - working_dir, + 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(), + selected_run_config: None, + run_output: Vec::new(), + run_running: false, + client: CoreClient::new(), + run_child: Arc::new(Mutex::new(None)), + run_seq: 0, git_log: Vec::new(), git_log_error: None, } @@ -83,6 +155,11 @@ impl BottomPanelView { self.refresh_git_log(cx); return; } + if tab == BottomTab::Run { + // 打开即重探 Run 工程;reload_run_project 内已 notify。 + self.reload_run_project(cx); + return; + } cx.notify(); } @@ -111,6 +188,232 @@ impl BottomPanelView { !self.run_history.is_empty() } + /// 更新工作目录并重探 Run 工程(替代直接写 `working_dir` 字段)。 + pub fn set_working_dir(&mut self, dir: String, cx: &mut Context) { + self.working_dir = dir; + self.reload_run_project(cx); + } + + /// 重探 Run 工程:本地 `.lithe/run/configurations.json` 优先命中即 `Ready`; + /// 否则先 `maven.scan`(payload `{root, paths}`)探测工程类型,再 + /// `runConfig.generate`(payload `{root}`,返回 + /// `{generated: {configurations[]}, entryCount}`)取配置列表,失败即 `Failed`。 + pub fn reload_run_project(&mut self, cx: &mut Context) { + self.run_seq += 1; + let seq = self.run_seq; + self.run_project_name = crate::settings::project_dir_name(&self.working_dir).to_string(); + let local = read_local_run_configs(&self.working_dir); + if !local.is_empty() { + self.run_configs = local; + self.keep_run_selection(); + self.run_state = RunProjectState::Ready; + cx.notify(); + return; + } + self.run_state = RunProjectState::Loading; + cx.notify(); + let client = self.client.clone(); + let root = self.working_dir.clone(); + cx.spawn(async move |this, cx| { + // 先探 Maven 工程(只作工程类型参考;失败不直接 Failed,交给 generate 定夺)。 + let _ = client + .execute::( + &cx, + "maven.scan", + serde_json::json!({ "root": root, "paths": [] }), + ) + .await; + let generated = client + .execute::( + &cx, + "runConfig.generate", + serde_json::json!({ "root": root }), + ) + .await; + let _ = this.update(cx, |view, cx| { + if view.run_seq != seq { + return; + } + match generated { + Ok(value) => { + view.run_configs = parse_generated_configs(&value); + // 无 LSP 时 generate 为空:本地文本扫描 main 方法兜底 + //(对齐 Tauri 入口点发现,Linux 无 JDT 故用源码文本匹配)。 + if view.run_configs.is_empty() { + view.run_configs = scan_java_mains(&root); + } + view.keep_run_selection(); + view.run_state = if view.run_configs.is_empty() { + RunProjectState::Missing + } else { + RunProjectState::Ready + }; + } + Err(err) => { + view.run_state = RunProjectState::Failed(err); + } + } + cx.notify(); + }); + }) + .detach(); + } + + /// 运行选中配置;运行中则改为停止。 + pub fn run_selected_config(&mut self, cx: &mut Context) { + if self.run_running { + self.stop_running(cx); + return; + } + let Some(item) = self.selected_run_item() else { + return; + }; + self.run_running = true; + push_run_line(&mut self.run_output, format!("开始运行:{}", item.name)); + cx.notify(); + let (tx, rx) = mpsc::channel::(); + let child_slot = self.run_child.clone(); + let client = self.client.clone(); + let root = self.working_dir.clone(); + cx.spawn(async move |this, cx| { + // 先问 core 要可执行计划(payload `{root, configurationId}`,返回 + // `{executable, arguments, workingDirectory}`);本地配置或工具链 + // 未解析时拿不到可直跑的命令,退化到按 kind 拼的本地命令。 + let plan = client + .execute::( + &cx, + "runConfig.createLaunchPlan", + serde_json::json!({ "root": root, "configurationId": item.id }), + ) + .await + .ok(); + let steps = plan_to_steps(plan.as_ref(), &item, &root); + let started = this.update(cx, |view, cx| { + if steps.is_empty() { + view.run_running = false; + push_run_line(&mut view.run_output, "无可执行步骤".to_string()); + cx.notify(); + return false; + } + let display = format!("$ {} {}", steps[0].program, steps[0].args.join(" ")); + push_run_line(&mut view.run_output, display.clone()); + view.record_run(&display, cx); + cx.notify(); + true + }); + if !matches!(started, Ok(true)) { + return; + } + std::thread::spawn(move || run_steps_blocking(steps, child_slot, tx)); + // 输出泵:每行经 background 线程阻塞收,再回到主线程落盘展示。 + let rx = Arc::new(Mutex::new(rx)); + loop { + let slot = rx.clone(); + let next = cx + .background_executor() + .spawn(async move { + let guard = slot.lock().ok()?; + guard.recv().ok() + }) + .await; + match next { + Some(line) => { + if this + .update(cx, |view, cx| { + push_run_line(&mut view.run_output, line); + cx.notify(); + }) + .is_err() + { + break; + } + } + None => break, + } + } + let _ = this.update(cx, |view, cx| { + view.run_running = false; + cx.notify(); + }); + }) + .detach(); + } + + /// 停止在跑进程(只 kill 直接子进程;mvn 拉起的 java 孙进程不在此列)。 + pub fn stop_running(&mut self, cx: &mut Context) { + if let Ok(mut slot) = self.run_child.lock() { + if let Some(child) = slot.as_mut() { + let _ = child.kill(); + } + } + push_run_line(&mut self.run_output, "已发送停止信号…".to_string()); + cx.notify(); + } + + /// 当前选中项(无选中或已失效时回退首项)。 + fn selected_run_item(&self) -> Option { + self.selected_run_config + .as_deref() + .and_then(|id| self.run_configs.iter().find(|c| c.id == id).cloned()) + .or_else(|| self.run_configs.first().cloned()) + } + + /// 保持选中有效:原选中仍在则不动,否则选首项。 + fn keep_run_selection(&mut self) { + let keep = self + .selected_run_config + .as_ref() + .is_some_and(|id| self.run_configs.iter().any(|c| &c.id == id)); + if !keep { + self.selected_run_config = self.run_configs.first().map(|c| c.id.clone()); + } + } + + /// Run 面板状态行文案。 + fn run_state_text(&self, cx: &gpui_kit::App) -> String { + let zh = crate::i18n::is_zh(cx); + match &self.run_state { + RunProjectState::Missing => { + if zh { + "无可运行配置".to_string() + } else { + "No runnable configurations".to_string() + } + } + RunProjectState::Loading => { + if zh { + "加载中…".to_string() + } else { + "Loading…".to_string() + } + } + RunProjectState::Ready => { + if self.run_running { + if zh { + "运行中".to_string() + } else { + "Running".to_string() + } + } else if zh { + format!("就绪 · {} 个配置", self.run_configs.len()) + } else { + format!("Ready · {} configurations", self.run_configs.len()) + } + } + RunProjectState::Failed(err) => { + if err.is_empty() { + if zh { + "加载失败".to_string() + } else { + "Failed to load".to_string() + } + } else { + err.clone() + } + } + } + } + /// 重新加载 Git 提交记录。core 的 `git.historyPage` 需经宿主异步接线, /// 这里用 `git -C log --oneline -50` 同步退化实现, /// 解析 hash + 首行 message 只读展示。 @@ -172,64 +475,229 @@ impl BottomPanelView { })) } - /// Run 面板:运行历史列表,每行重跑按钮经 terminal 重放命令。 + /// Run 面板:Tauri RunPane 子集——顶部状态行(工程名+状态+运行/停止+ + /// 重扫+清空)+ 配置列表(选中高亮)+ 输出区(monospace)。 fn render_run_panel(&self, cx: &mut Context) -> AnyElement { - if self.run_history.is_empty() { - return div() - .size_full() - .flex() - .items_center() - .justify_center() - .text_xs() - .text_color(ThemeColors::text_muted()) - .child(run_empty_text(cx)) - .into_any_element(); + 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| { + let root = this.working_dir.clone(); + let _ = append_default_run_config(&root); + this.reload_run_project(cx); + })), + ) + .into_any_element(); + } + } } + let running = self.run_running; + let run_label = if running { + run_ui_text(cx, "停止", "Stop") + } else { + run_ui_text(cx, "运行", "Run") + }; + let can_run = !matches!(self.run_state, RunProjectState::Loading); + let status = format!("{} · {}", self.run_project_name, self.run_state_text(cx)); + let rows: Vec = self - .run_history + .run_configs .iter() - .enumerate() - .map(|(idx, cmd)| { - let rerun = cmd.clone(); + .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-history-{idx}")) - .h(px(26.0)) + .id(format!("run-config-{}", item.id)) .w_full() .items_center() - .gap_2() - .px_3() + .gap_1p5() + .px_2() + .py_1() .rounded_sm() + .cursor_pointer() .text_xs() - .text_color(ThemeColors::text_primary()) - .hover(|h| h.bg(ThemeColors::bg_tab_hover())) + .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( - div() + v_flex() .flex_1() - .truncate() - .font_family("monospace") - .child(cmd.clone()), + .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-rerun-{idx}")) + 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| { - let _ = - this.terminal.update(cx, |t, cx| t.send_command(&rerun, cx)); + this.selected_run_config = Some(run_id.clone()); + this.run_selected_config(cx); })), ) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.selected_run_config = Some(id.clone()); + cx.notify(); + })) .into_any_element() }) .collect(); - div() - .flex_1() - .w_full() - .overflow_y_scrollbar() - .py_1() - .children(rows) + 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(run_ui_text(cx, "暂无输出", "No output")) + .into_any_element()] + } else { + self.run_output[start..] + .iter() + .map(|line| div().child(line.clone()).into_any_element()) + .collect() + }; + + v_flex() + .size_full() + .child( + h_flex() + .flex_shrink_0() + .h(px(30.0)) + .w_full() + .items_center() + .gap_2() + .px_3() + .border_b_1() + .border_color(ThemeColors::border()) + .child( + div() + .flex_1() + .truncate() + .text_xs() + .text_color(ThemeColors::text_primary()) + .child(status), + ) + .child( + Button::new("run-toggle") + .small() + .primary() + .disabled(!can_run) + .label(run_label) + .on_click(cx.listener(|this, _event, _window, cx| { + this.run_selected_config(cx); + })), + ) + .child( + Button::new("run-rescan") + .small() + .ghost() + .disabled(running) + .label(run_ui_text(cx, "重扫", "Rescan")) + .on_click(cx.listener(|this, _event, _window, cx| { + this.reload_run_project(cx); + })), + ) + .child( + Button::new("run-clear-output") + .small() + .ghost() + .label(run_ui_text(cx, "清空输出", "Clear")) + .on_click(cx.listener(|this, _event, _window, cx| { + this.run_output.clear(); + cx.notify(); + })), + ), + ) + .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), + ) + .child( + div() + .flex_1() + .h_full() + .overflow_y_scrollbar() + .p_2() + .font_family("monospace") + .text_xs() + .text_color(ThemeColors::text_primary()) + .children(output), + ), + ) .into_any_element() } @@ -386,6 +854,7 @@ impl Render for BottomPanelView { } BottomTab::Run => { this.run_history.clear(); + this.run_output.clear(); cx.notify(); } BottomTab::Diagnostics => { @@ -482,11 +951,501 @@ fn load_git_log(workdir: &str) -> Result, String> { Ok(entries) } -fn run_empty_text(cx: &gpui_kit::App) -> &'static str { +/// 读本地运行配置(`/.lithe/run/configurations.json`),兼容数组与 +/// `{configurations: []}` 两种形状;字段沿用 v1 形态(`name/type/mainClass`)。 +fn read_local_run_configs(workspace_root: &str) -> Vec { + let path = std::path::Path::new(workspace_root) + .join(".lithe") + .join("run") + .join("configurations.json"); + let Ok(text) = std::fs::read_to_string(&path) else { + return Vec::new(); + }; + let Ok(value) = serde_json::from_str::(&text) else { + return Vec::new(); + }; + let arr = if let Some(arr) = value.as_array() { + arr.clone() + } else if let Some(arr) = value.get("configurations").and_then(|v| v.as_array()) { + arr.clone() + } else { + return Vec::new(); + }; + arr.iter() + .filter_map(|item| { + let name = item.get("name")?.as_str()?; + let kind_raw = item.get("type").and_then(|t| t.as_str()).unwrap_or("java"); + let main_class = item + .get("mainClass") + .and_then(|m| m.as_str()) + .map(str::to_string); + let kind = if kind_raw.contains("npm") || kind_raw.contains("node") { + "npm" + } else if kind_raw.contains("maven") { + "maven" + } else { + "mainClass" + }; + let detail = main_class.clone().unwrap_or_else(|| kind_raw.to_string()); + Some(RunConfigItem { + id: name.to_string(), + name: name.to_string(), + kind: kind.to_string(), + detail, + main_class, + source: None, + }) + }) + .collect() +} + +/// 追加一个默认 Java 运行配置(保留已有配置),供 Missing 空态的生成按钮用。 +fn append_default_run_config(workspace_root: &str) -> bool { + let path = std::path::Path::new(workspace_root) + .join(".lithe") + .join("run") + .join("configurations.json"); + if let Some(parent) = path.parent() { + if std::fs::create_dir_all(parent).is_err() { + return false; + } + } + let mut list: Vec = std::fs::read_to_string(&path) + .ok() + .and_then(|text| serde_json::from_str::(&text).ok()) + .and_then(|v| { + if let Some(arr) = v.as_array() { + Some(arr.clone()) + } else { + v.get("configurations").and_then(|c| c.as_array()).cloned() + } + }) + .unwrap_or_default(); + list.push(serde_json::json!({"name": "Run Main", "type": "java", "mainClass": "Main"})); + let value = serde_json::json!({"configurations": list}); + serde_json::to_string_pretty(&value) + .ok() + .and_then(|text| std::fs::write(&path, text).ok()) + .is_some() +} + +/// 解析 `runConfig.generate` 返回的 `{generated: {configurations[]}}`。 +/// 本地 Java 入口扫描:无 LSP 时的兜底(对齐 Tauri `discoverJavaEntrypoints`, +/// Linux 无 JDT 故用源码文本匹配 `public static void main`)。 +/// 主类由 `package` 声明 + 文件名推导;上限 20 个,按路径排序保证稳定。 +fn scan_java_mains(root: &str) -> Vec { + const MAX_MAINS: usize = 20; + const MAX_DEPTH: usize = 8; + const SKIP_DIRS: &[&str] = &[ + "target", + "build", + "out", + "dist", + "node_modules", + ".git", + ".idea", + ".vscode", + "vendor", + ]; + const MAX_FILE_BYTES: u64 = 512 * 1024; + + fn walk(dir: &std::path::Path, depth: usize, out: &mut Vec) { + if depth > MAX_DEPTH || out.len() >= MAX_MAINS * 4 { + return; + } + let Ok(entries) = std::fs::read_dir(dir) else { + return; + }; + let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect(); + entries.sort_by_key(|e| e.file_name()); + for entry in entries { + let path = entry.path(); + let name = entry.file_name().to_string_lossy().to_string(); + if path.is_dir() { + if !name.starts_with('.') && !SKIP_DIRS.contains(&name.as_str()) { + walk(&path, depth + 1, out); + } + } else if name.ends_with(".java") { + out.push(path); + } + } + } + + fn package_of(text: &str) -> Option { + text.lines().find_map(|line| { + let line = line.trim(); + line.strip_prefix("package ") + .and_then(|rest| rest.strip_suffix(';')) + .map(|pkg| pkg.trim().to_string()) + .filter(|pkg| { + !pkg.is_empty() + && pkg + .chars() + .all(|c| c.is_alphanumeric() || c == '.' || c == '_') + }) + }) + } + + let root_path = std::path::Path::new(root); + let mut files = Vec::new(); + walk(root_path, 0, &mut files); + let mut items = Vec::new(); + for path in files { + if items.len() >= MAX_MAINS { + break; + } + if std::fs::metadata(&path).is_ok_and(|m| m.len() > MAX_FILE_BYTES) { + continue; + } + let Ok(text) = std::fs::read_to_string(&path) else { + continue; + }; + if !text.contains("public static void main") { + continue; + } + let stem = path + .file_stem() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_default(); + if stem.is_empty() { + continue; + } + let main_class = match package_of(&text) { + Some(pkg) => format!("{pkg}.{stem}"), + None => stem, + }; + let source = path + .strip_prefix(root_path) + .map(|p| p.to_string_lossy().replace('\\', "/")) + .unwrap_or_default(); + items.push(RunConfigItem { + id: format!("local-main:{main_class}"), + name: main_class.clone(), + kind: "mainClass".to_string(), + detail: main_class.clone(), + main_class: Some(main_class), + source: Some(source), + }); + } + items.sort_by(|a, b| a.name.cmp(&b.name)); + items +} + +fn parse_generated_configs(value: &serde_json::Value) -> Vec { + value + .get("generated") + .and_then(|g| g.get("configurations")) + .and_then(|c| c.as_array()) + .map(|arr| arr.iter().filter_map(config_item_from_generated).collect()) + .unwrap_or_default() +} + +fn config_item_from_generated(item: &serde_json::Value) -> Option { + let id = item.get("id")?.as_str()?; + let name = item.get("name").and_then(|n| n.as_str()).unwrap_or(id); + let provider = item.get("provider").and_then(|p| p.as_str()).unwrap_or(""); + let command = item.get("command").and_then(|c| c.as_str()).unwrap_or(""); + let maven = &item["extensions"]["maven"]; + let main_class = maven + .get("mainClass") + .and_then(|m| m.as_str()) + .map(str::to_string); + let module = maven + .get("module") + .and_then(|m| m.as_str()) + .map(str::to_string); + let source = item + .get("extensions") + .and_then(|e| e.get("java")) + .and_then(|j| j.get("source")) + .and_then(|s| s.as_str()) + .map(str::to_string); + let kind = if provider.contains("npm") || provider.contains("node") || command == "npm" { + "npm" + } else if provider.contains("maven") { + "maven" + } else { + "mainClass" + }; + let detail = if kind == "npm" { + item.get("args") + .and_then(|a| a.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str()) + .collect::>() + .join(" ") + }) + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| "run dev".to_string()) + } else { + main_class + .clone() + .or(module.clone()) + .unwrap_or_else(|| provider.to_string()) + }; + Some(RunConfigItem { + id: id.to_string(), + name: name.to_string(), + kind: kind.to_string(), + detail, + main_class, + source, + }) +} + +/// 由 `createLaunchPlan` 结果拼直跑步骤:只有 `executable.command` +/// 是真实可执行路径;`executable.toolchain` 需宿主解析,拿不到就走退化。 +fn plan_to_steps( + plan: Option<&serde_json::Value>, + item: &RunConfigItem, + root: &str, +) -> Vec { + if let Some(plan) = plan { + let cwd = plan + .get("workingDirectory") + .and_then(|v| v.as_str()) + .unwrap_or("."); + let cwd = if cwd == "." || cwd.is_empty() { + root.to_string() + } else { + format!("{root}/{cwd}") + }; + if let Some(command) = plan + .get("executable") + .and_then(|e| e.get("command")) + .and_then(|c| c.as_str()) + .filter(|c| !c.is_empty()) + { + let args = plan + .get("arguments") + .and_then(|a| a.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str().map(str::to_string)) + .collect() + }) + .unwrap_or_default(); + return vec![RunStep { + program: command.to_string(), + args, + cwd, + }]; + } + } + fallback_run_steps(item, root) +} + +/// 退化执行规则:maven 用 mvn 跑 `compile exec:java`;npm 跑 `run dev`; +/// java 单文件走 javac+java,否则直跑主类。 +fn fallback_run_steps(item: &RunConfigItem, root: &str) -> Vec { + match item.kind.as_str() { + "maven" => { + let mut args = vec!["-B".to_string(), "-ntp".to_string(), "compile".to_string()]; + if let Some(main) = item.main_class.as_deref().filter(|s| !s.is_empty()) { + args.push("exec:java".to_string()); + args.push(format!("-Dexec.mainClass={main}")); + } + vec![RunStep { + program: resolve_maven_executable(root), + args, + cwd: root.to_string(), + }] + } + "npm" => vec![RunStep { + program: "npm".to_string(), + args: vec!["run".to_string(), pick_npm_script(root)], + cwd: root.to_string(), + }], + _ => { + let classes = format!("{root}/.lithe/run/classes"); + if let (Some(source), Some(main)) = (item.source.as_deref(), item.main_class.as_deref()) + { + let abs = format!("{root}/{source}"); + if !source.is_empty() && !main.is_empty() && std::path::Path::new(&abs).is_file() { + let _ = std::fs::create_dir_all(&classes); + return vec![ + RunStep { + program: "javac".to_string(), + args: vec!["-d".to_string(), classes.clone(), abs], + cwd: root.to_string(), + }, + RunStep { + program: "java".to_string(), + args: vec!["-cp".to_string(), classes, main.to_string()], + cwd: root.to_string(), + }, + ]; + } + } + let main = item.main_class.clone().unwrap_or_else(|| item.name.clone()); + vec![RunStep { + program: "java".to_string(), + args: vec![main], + cwd: root.to_string(), + }] + } + } +} + +/// mvn 路径:先看 `.lithe/run/local.json` 的 `mavenExecutablePath` +///(含 `toolchain` 下),没有就用 `PATH` 里的 `mvn`。 +fn resolve_maven_executable(root: &str) -> String { + let path = std::path::Path::new(root) + .join(".lithe") + .join("run") + .join("local.json"); + let from_file = std::fs::read_to_string(&path) + .ok() + .and_then(|text| serde_json::from_str::(&text).ok()) + .and_then(|value| { + value + .get("mavenExecutablePath") + .or_else(|| { + value + .get("toolchain") + .and_then(|t| t.get("mavenExecutablePath")) + }) + .and_then(|v| v.as_str()) + .map(str::to_string) + }) + .filter(|s| !s.trim().is_empty()); + from_file.unwrap_or_else(|| "mvn".to_string()) +} + +/// npm 脚本选择:`package.json` 有 dev 用 dev,否则 start,再没有还用 dev(报错由输出展示)。 +fn pick_npm_script(root: &str) -> String { + let has = |name: &str, value: &serde_json::Value| { + value.get("scripts").and_then(|s| s.get(name)).is_some() + }; + std::fs::read_to_string(std::path::Path::new(root).join("package.json")) + .ok() + .and_then(|text| serde_json::from_str::(&text).ok()) + .map(|value| { + if has("dev", &value) { + "dev".to_string() + } else if has("start", &value) { + "start".to_string() + } else { + "dev".to_string() + } + }) + .unwrap_or_else(|| "dev".to_string()) +} + +/// 顺序跑步骤并把输出行推入 channel;首个失败步骤后停。stdout/stderr 各一根 +/// 转发线程;子进程句柄共享给停止按钮,`try_wait` 短锁轮询避免 `wait` +/// 占锁导致 kill 拿不到锁。 +fn run_steps_blocking( + steps: Vec, + child_slot: Arc>>, + tx: mpsc::Sender, +) { + use std::io::BufRead as _; + for step in &steps { + let _ = tx.send(format!("$ {} {}", step.program, step.args.join(" "))); + let mut child = match std::process::Command::new(&step.program) + .args(&step.args) + .current_dir(&step.cwd) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + { + Ok(child) => child, + Err(err) => { + let _ = tx.send(format!("启动失败:{err}")); + return; + } + }; + let stdout = child.stdout.take(); + let stderr = child.stderr.take(); + if let Ok(mut slot) = child_slot.lock() { + *slot = Some(child); + } + let tx_out = tx.clone(); + let out_handle = stdout.map(|out| { + std::thread::spawn(move || { + for line in std::io::BufReader::new(out).lines().map_while(Result::ok) { + if tx_out.send(line).is_err() { + break; + } + } + }) + }); + let tx_err = tx.clone(); + let err_handle = stderr.map(|err| { + std::thread::spawn(move || { + for line in std::io::BufReader::new(err).lines().map_while(Result::ok) { + if tx_err.send(line).is_err() { + break; + } + } + }) + }); + let status = loop { + let Ok(mut slot) = child_slot.lock() else { + break None; + }; + match slot.as_mut().map(|child| child.try_wait()) { + Some(Ok(Some(status))) => break Some(status), + Some(Ok(None)) => { + drop(slot); + std::thread::sleep(std::time::Duration::from_millis(50)); + } + _ => break None, + } + }; + if let Some(handle) = out_handle { + let _ = handle.join(); + } + if let Some(handle) = err_handle { + let _ = handle.join(); + } + if let Ok(mut slot) = child_slot.lock() { + let _ = slot.take(); + } + match status { + Some(status) if status.success() => { + let _ = tx.send("退出码:0".to_string()); + } + Some(status) => { + let _ = tx.send(format!("进程已退出:{status}")); + return; + } + None => { + let _ = tx.send("运行已结束".to_string()); + return; + } + } + } +} + +/// 输出行入列(超限丢最旧)。 +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 { - "No run history" + en.to_string() } } diff --git a/linux/src/workbench/maven.rs b/linux/src/workbench/maven.rs index c0dc017d9..ce6ea2333 100644 --- a/linux/src/workbench/maven.rs +++ b/linux/src/workbench/maven.rs @@ -1,21 +1,28 @@ -//! 右侧 Maven 导航工具窗口:项目树 + 生命周期阶段,对齐 Tauri +//! 右侧 Maven 导航工具窗口:项目树 + 生命周期阶段 + 依赖 + 自定义 goal,对齐 Tauri //! `windows/tauri/src/features/maven/` 的导航语义。 //! -//! 本面板只负责导航展示与事件发射:双击/点击阶段行发射 -//! [`MavenEvent::RunGoal`],实际命令执行由外部经底部 Terminal 完成。 +//! 本面板只负责导航展示与事件发射:点击阶段行 / 自定义 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::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, Icon, Sizable 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, Context, EventEmitter, FontWeight, InteractiveElement as _, IntoElement, - ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Window, + div, px, AppContext as _, 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 生命周期阶段。 @@ -81,11 +88,23 @@ fn lifecycle_key(pom: &str) -> String { format!("{pom}#lifecycle") } +/// 依赖组的展开键。 +fn deps_key(pom: &str) -> String { + format!("{pom}#deps") +} + /// phase 行的稳定节点 id,同时用作选中键。 fn phase_id(pom: &str, phase: &str) -> String { format!("{pom}#lifecycle#{phase}") } +/// 从选中行 id 反推所属 pom:只有 phase 行可反推,其余返回空。 +fn selected_pom(selected: &Option) -> Option { + selected + .as_ref() + .and_then(|id| id.split_once("#lifecycle#").map(|(pom, _)| pom.to_string())) +} + /// pom 行的显示路径:相对 root,根 pom 显示 `pom.xml`。 fn display_pom_path(root: &str, pom: &str) -> String { std::path::Path::new(pom) @@ -101,6 +120,81 @@ fn display_pom_path(root: &str, pom: &str) -> String { .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() +} + +/// pom 所在目录相对 root 的模块路径:根 pom 为 `.`,与 core 的模块路径约定一致。 +fn module_path_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 `projectStatus` 的工程状态:`idle` / `loading` / `ready` / `failed`。 +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MavenStatus { + Idle, + Loading, + Ready, + Failed(String), +} + +/// 依赖列表行的最小展示字段,来自 `maven.dependencies` 返回的 +/// `dependencies[]`(`groupId` / `artifactId` / `version` / `scope`)。 +#[derive(Debug, Clone)] +pub struct MavenDep { + pub group: String, + pub artifact: String, + pub version: String, + pub scope: String, +} + +/// 从 `maven.dependencies` 的返回体解析依赖列表,缺失字段的行直接丢弃。 +fn parse_deps(data: &serde_json::Value) -> Vec { + data.get("dependencies") + .and_then(|value| value.as_array()) + .map(|items| { + items + .iter() + .filter_map(|item| { + 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(), + scope: item + .get("scope") + .and_then(|value| value.as_str()) + .unwrap_or_default() + .to_string(), + }) + }) + .collect() + }) + .unwrap_or_default() +} + /// Maven 导航面板派发的事件:执行只发射,由外部经底部 Terminal 执行。 #[derive(Debug, Clone)] pub enum MavenEvent { @@ -108,37 +202,64 @@ pub enum MavenEvent { Close, } -/// 右侧 Maven 导航工具窗口:项目树 + 生命周期阶段。 +/// 右侧 Maven 导航工具窗口:项目树 + 生命周期阶段 + 依赖 + 自定义 goal。 pub struct MavenView { pub root: String, poms: Vec, expanded: HashSet, selected: Option, + project_status: MavenStatus, + project_name: Option, + dependencies: Vec, + dep_status: MavenStatus, + /// 一次只展开一个模块的依赖组,存 pom 绝对路径。 + expanded_deps: Option, + custom_goal: String, + goal_input: Option>, + _goal_subscription: Option, + client: CoreClient, + scan_seq: u64, + dep_seq: u64, } impl EventEmitter for MavenView {} impl MavenView { - pub fn new(root: String, _cx: &mut Context) -> Self { + pub fn new(root: String, cx: &mut Context) -> Self { let mut view = Self { root, poms: Vec::new(), expanded: HashSet::new(), selected: None, + project_status: MavenStatus::Idle, + project_name: None, + dependencies: Vec::new(), + dep_status: MavenStatus::Idle, + expanded_deps: None, + custom_goal: String::new(), + goal_input: None, + _goal_subscription: None, + client: CoreClient::new(), + scan_seq: 0, + dep_seq: 0, }; - view.rescan(); + view.reload(cx); view } pub fn set_root(&mut self, root: String, cx: &mut Context) { self.root = root; - self.rescan(); - cx.notify(); + self.expanded_deps = None; + self.dependencies.clear(); + self.dep_status = MavenStatus::Idle; + self.reload(cx); } pub fn refresh(&mut self, cx: &mut Context) { - self.rescan(); - cx.notify(); + self.expanded_deps = None; + self.dependencies.clear(); + self.dep_status = MavenStatus::Idle; + self.reload(cx); } pub fn has_projects(&self) -> bool { @@ -155,23 +276,183 @@ impl MavenView { } } + /// 本地重扫 + core `maven.scan` 拿模块结构: + /// payload `{root, paths}`(相对 pom 路径),返回 `MavenScanResponse` + ///(`relativePath` / `artifactId` / `modules` / `profiles` …)或 `null` + ///(未检出工程)。失败只落 `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_name = data + .get("artifactId") + .and_then(|value| value.as_str()) + .map(str::to_string); + view.project_status = MavenStatus::Ready; + } + Err(err) => { + view.project_name = None; + 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(); } + + /// 依赖组一次只展开一个:收起直接关,展开则触发依赖加载。 + fn toggle_deps(&mut self, pom: &str, cx: &mut Context) { + if self.expanded_deps.as_deref() == Some(pom) { + self.expanded_deps = None; + cx.notify(); + } else { + self.expanded_deps = Some(pom.to_string()); + self.load_deps(pom, cx); + } + } + + /// 先 `maven.dependencyPlan` 校验模块并拿出确定的离线调用计划 + /// (payload `{root, context: {version: 1, reactorPath: "."}, module}`, + /// 返回 `MavenLaunchPlanResponse`),再 `maven.dependencies` + /// (payload `{modulePath, output}`,返回 `{modulePath, dependencies[]}`) + /// 解析依赖列表。Linux 侧无 Maven 进程宿主,`output` 为空即返回空列表; + /// 真实树需在终端运行 `dependency:tree`,失败文案直接展示。 + fn load_deps(&mut self, pom: &str, cx: &mut Context) { + self.dep_seq += 1; + let seq = self.dep_seq; + self.dep_status = MavenStatus::Loading; + self.dependencies.clear(); + cx.notify(); + + let client = self.client.clone(); + let root = self.root.clone(); + let module = module_path_for_pom(&self.root, pom); + cx.spawn(async move |this, cx| { + let plan = client + .execute::( + &cx, + "maven.dependencyPlan", + serde_json::json!({ + "root": root, + "context": { "version": 1, "reactorPath": "." }, + "module": module, + }), + ) + .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 != seq { + return; + } + match outcome { + Ok(deps) => { + view.dependencies = deps; + view.dep_status = MavenStatus::Ready; + } + Err(err) => { + view.dep_status = 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 + } } impl Render for MavenView { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + 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(); + } + }, + )); + } + // 快照后构建行,避免在 `.children` 闭包里同时借用 self 与 cx。 let root = self.root.clone(); let poms = self.poms.clone(); let expanded = self.expanded.clone(); let selected = self.selected.clone(); + let project_status = self.project_status.clone(); + let project_name = self.project_name.clone(); + let expanded_deps = self.expanded_deps.clone(); + let dep_status = self.dep_status.clone(); + let dependencies = self.dependencies.clone(); + let custom_goal = self.custom_goal.clone(); let has_projects = !poms.is_empty(); + let title_text = match &project_name { + Some(name) => format!("{} · {name}", crate::i18n::menu_text(cx, "maven.title")), + None => crate::i18n::menu_text(cx, "maven.title").to_string(), + }; + let status_banner: Option<(bool, String)> = match &project_status { + MavenStatus::Loading => Some((false, "正在扫描 Maven 项目…".to_string())), + MavenStatus::Failed(err) => Some((true, err.clone())), + MavenStatus::Idle | MavenStatus::Ready => None, + }; + // 自定义 goal 的执行目标:选中 phase 行归属的 pom,否则首个 pom。 + let goal_target = selected_pom(&selected).or_else(|| poms.first().cloned()); + let mut rows = Vec::new(); for pom in &poms { let pom_expanded = expanded.contains(pom); @@ -263,56 +544,164 @@ impl Render for MavenView { })) .into_any_element(), ); - if !group_expanded { - continue; + if group_expanded { + for phase in MAVEN_LIFECYCLE_PHASES { + let row_id = phase_id(pom, phase); + let is_selected = selected.as_deref() == Some(row_id.as_str()); + rows.push( + h_flex() + .id(row_id.clone()) + .h(px(24.0)) + .w_full() + .items_center() + .cursor_pointer() + .rounded_sm() + .pl(px(30.0)) + .pr_2() + .gap_1p5() + .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(ThemeColors::text_muted()), + ) + .child(div().flex_1().truncate().child(phase)) + .on_click(cx.listener({ + let pom = pom.clone(); + let phase = phase.to_string(); + move |this, _event, _window, cx| { + this.selected = Some(phase_id(&pom, &phase)); + cx.emit(MavenEvent::RunGoal { + pom_path: pom.clone(), + phase: phase.clone(), + }); + cx.notify(); + } + })) + .into_any_element(), + ); + } } - for phase in MAVEN_LIFECYCLE_PHASES { - let row_id = phase_id(pom, phase); - let is_selected = selected.as_deref() == Some(row_id.as_str()); - rows.push( - h_flex() - .id(row_id.clone()) - .h(px(24.0)) - .w_full() - .items_center() - .cursor_pointer() - .rounded_sm() - .pl(px(30.0)) - .pr_2() - .gap_1p5() - .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(ThemeColors::text_muted()), - ) - .child(div().flex_1().truncate().child(phase)) - .on_click(cx.listener({ - let pom = pom.clone(); - let phase = phase.to_string(); - move |this, _event, _window, cx| { - this.selected = Some(phase_id(&pom, &phase)); - cx.emit(MavenEvent::RunGoal { - pom_path: pom.clone(), - phase: phase.clone(), - }); - cx.notify(); - } - })) - .into_any_element(), - ); + // 依赖组:一次只展开一个模块,点击触发 dependencyPlan + dependencies。 + let deps_open = expanded_deps.as_deref() == Some(pom.as_str()); + let deps_chevron = if deps_open { + IconName::ChevronDown + } else { + IconName::ChevronRight + }; + rows.push( + h_flex() + .id(deps_key(pom)) + .h(px(24.0)) + .w_full() + .items_center() + .cursor_pointer() + .rounded_sm() + .pl(px(18.0)) + .pr_2() + .gap_1p5() + .text_xs() + .text_color(ThemeColors::text_primary()) + .hover(|h| h.bg(ThemeColors::bg_tab_hover())) + .child( + Icon::new(deps_chevron) + .size(px(12.0)) + .text_color(ThemeColors::text_muted()), + ) + .child( + Icon::new(IconName::Package) + .size(px(14.0)) + .text_color(ThemeColors::accent_blue()), + ) + .child(div().flex_1().truncate().child("依赖")) + .on_click(cx.listener({ + let pom = pom.clone(); + move |this, _event, _window, cx| { + this.toggle_deps(&pom, cx); + } + })) + .into_any_element(), + ); + if deps_open { + match &dep_status { + MavenStatus::Loading => { + rows.push( + div() + .w_full() + .pl(px(30.0)) + .pr_2() + .py_1() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child("正在加载依赖…") + .into_any_element(), + ); + } + MavenStatus::Failed(err) => { + rows.push( + div() + .w_full() + .pl(px(30.0)) + .pr_2() + .py_1() + .text_xs() + .text_color(ThemeColors::accent_red()) + .child(err.clone()) + .into_any_element(), + ); + } + MavenStatus::Idle | MavenStatus::Ready if dependencies.is_empty() => { + rows.push( + div() + .w_full() + .pl(px(30.0)) + .pr_2() + .py_1() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child("暂无依赖数据") + .into_any_element(), + ); + } + MavenStatus::Idle | MavenStatus::Ready => { + for (index, dep) in dependencies.iter().enumerate() { + let label = format!( + "{}:{}:{} [{}]", + dep.group, dep.artifact, dep.version, dep.scope + ); + rows.push( + h_flex() + .id(format!("{pom}#dep#{index}")) + .w_full() + .items_center() + .rounded_sm() + .pl(px(30.0)) + .pr_2() + .py_1() + .gap_1p5() + .text_xs() + .text_color(ThemeColors::text_primary()) + .hover(|h| h.bg(ThemeColors::bg_tab_hover())) + .child(div().flex_1().truncate().child(label)) + .into_any_element(), + ); + } + } + } } } + let goal_entity_for_run = goal_entity.clone(); + let goal_target_for_run = goal_target.clone(); v_flex() .size_full() .bg(ThemeColors::bg_sidebar()) @@ -333,7 +722,7 @@ impl Render for MavenView { .text_xs() .font_weight(FontWeight::BOLD) .text_color(ThemeColors::text_muted()) - .child(crate::i18n::menu_text(cx, "maven.title")), + .child(title_text), ) .child( h_flex() @@ -361,6 +750,23 @@ impl Render for MavenView { ), ), ) + .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(if has_projects { div() .flex_1() @@ -380,5 +786,42 @@ impl Render for MavenView { .child(crate::i18n::menu_text(cx, "maven.noProjects")) .into_any_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("执行自定义 goal") + .disabled( + custom_goal.trim().is_empty() || goal_target_for_run.is_none(), + ) + .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; + } + if let Some(pom) = goal_target_for_run.clone() { + cx.emit(MavenEvent::RunGoal { + pom_path: pom, + phase: goal, + }); + } + })), + ), + ) } } diff --git a/linux/src/workbench/settings_dialog.rs b/linux/src/workbench/settings_dialog.rs index 2f1ee2d7c..8342ceac5 100644 --- a/linux/src/workbench/settings_dialog.rs +++ b/linux/src/workbench/settings_dialog.rs @@ -1863,14 +1863,86 @@ impl SettingsDialog { .child( self.render_group( crate::i18n::menu_text(cx, "settings.mac.detectedServers").to_string(), - self.render_note( - crate::i18n::menu_text(cx, "settings.mac.detectedServersDescription") - .to_string(), - ), + v_flex() + .w_full() + .gap_3() + .child( + self.render_note( + crate::i18n::menu_text( + cx, + "settings.mac.detectedServersDescription", + ) + .to_string(), + ), + ) + .children(Self::detected_language_servers().into_iter().map( + |(name, path)| { + self.render_row( + name.to_string(), + None, + self.render_value(path.unwrap_or_else(|| "未安装".to_string())), + ) + }, + )) + .child(self.render_note(Self::lsp_status_note())), ), ) } + /// PATH 中探测语言服务器可执行文件,`OnceLock` 缓存避免每次渲染重复遍历。 + /// 只做存在性展示,不触发任何启动。 + fn detected_language_servers() -> Vec<(&'static str, Option)> { + static CACHE: std::sync::OnceLock)>> = + std::sync::OnceLock::new(); + CACHE + .get_or_init(|| { + ["jdtls", "clangd", "rust-analyzer", "pyright"] + .into_iter() + .map(|name| (name, Self::find_in_path(name))) + .collect() + }) + .clone() + } + + /// 在 `PATH` 中查找可执行文件,返回完整路径。 + 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(|m| m.permissions().mode() & 0o111 != 0) + .unwrap_or(false); + if !executable { + continue; + } + } + return Some(candidate.to_string_lossy().into_owned()); + } + None + } + + /// 诚实状态说明:JDTLS 缺失则不伪造启动,仅说明缺失项。 + fn lsp_status_note() -> String { + let servers = Self::detected_language_servers(); + let jdtls_missing = servers + .iter() + .any(|(name, path)| *name == "jdtls" && path.is_none()); + if jdtls_missing { + "JDTLS 未安装:lsp.startServer 要求 executablePath/rootUri/workingDirectory,\ + Java 还需 jdtlsLaunchResources(launcher jar/配置目录/lombok),本机均缺失,\ + 故不启动;编辑器暂无诊断/悬停/补全接入。" + .to_string() + } else { + "已检测到服务器但尚未接入:编辑器暂无诊断/悬停/补全客户端,未自动启动。".to_string() + } + } + /// AI 聊天与编辑:提供商/模型/自动补全开关,全部落盘 ///(对齐 Tauri `AISettings` 的 Lithe Agent 分组)。 fn render_ai_content( diff --git a/linux/src/workbench/view.rs b/linux/src/workbench/view.rs index 9fc7731fe..c050cc113 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -905,8 +905,8 @@ impl WorkbenchView { let _ = self.maven.update(cx, |m, cx| { m.set_root(path.clone(), cx); }); - let _ = self.bottom_panel.update(cx, |bp, _cx| { - bp.working_dir = path.clone(); + let _ = self.bottom_panel.update(cx, |bp, cx| { + bp.set_working_dir(path.clone(), cx); }); // 右侧通知中心投递项目打开事件(对齐 Tauri 系统事件通知)。 let _ = self.notifications.update(cx, |n, cx| { From 97ad639ebebbd9324c1f027aa523d5d121c4a2e1 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 00:14:46 +0800 Subject: [PATCH 31/59] =?UTF-8?q?fix(linux):=20=E5=8F=B3=E4=BE=A7=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E7=AA=97=E5=8F=AF=E5=B7=A6=E5=8F=B3=E6=8B=96=E5=8A=A8?= =?UTF-8?q?=E7=BC=A9=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 主工作区编辑区+右侧改入 h_resizable workbench-center - 右侧 size_range 240-600,初值读 right_tool_window_width - on_resize 写回设置,隐藏时单面板无手柄 --- linux/src/workbench/view.rs | 87 +++++++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 28 deletions(-) diff --git a/linux/src/workbench/view.rs b/linux/src/workbench/view.rs index c050cc113..8981d4504 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -3,6 +3,7 @@ //! 结构对齐 Tauri `MainLayout`:顶栏 / 多项目标签条 / 活动栏 + 侧边栏 + 编辑区 + //! 右侧插件活动栏 / 底部面板 / 状态栏;无项目时显示欢迎页,浮层由模态状态控制。 +use gpui_kit::component::resizable::{h_resizable, resizable_panel}; use gpui_kit::component::{h_flex, v_flex}; use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::{ @@ -20,7 +21,7 @@ use crate::workbench::activity_rail::{ use crate::workbench::bottom_panel::{BottomPanelView, BottomTab}; use crate::workbench::branch_manager::{BranchManagerEvent, BranchManagerView}; use crate::workbench::command_palette::{CommandPaletteEvent, CommandPaletteModal}; -use crate::workbench::editor::EditorView; +use crate::workbench::editor::{EditorTabEvent, EditorView}; use crate::workbench::extensions_panel::{ExtensionsEvent, ExtensionsView}; use crate::workbench::go_to_line::{GoToLineEvent, GoToLineModal}; use crate::workbench::maven::{MavenEvent, MavenView}; @@ -577,6 +578,17 @@ impl WorkbenchView { }, ); + // 8b. 订阅编辑器标签页事件(在终端中打开) + let sub_editor_tab = + cx.subscribe( + &editor, + move |this, _ed, event: &EditorTabEvent, cx| match event { + EditorTabEvent::OpenInTerminal { dir } => { + this.send_terminal_command(&format!("cd \"{dir}\""), cx); + } + }, + ); + // 9. 订阅设置对话框事件 let sub_settings = cx.subscribe( &settings_dialog, @@ -752,6 +764,7 @@ impl WorkbenchView { sub_quick_open, sub_go_to_line, sub_command_palette, + sub_editor_tab, sub_settings, sub_project_dialog, sub_branch_manager, @@ -1690,7 +1703,50 @@ impl WorkbenchView { fn render_workbench(&self, show_status_bar: bool, cx: &mut Context) -> impl IntoElement { let bottom_visible = self.bottom_panel.read(cx).is_visible(); let bottom_splitter = self.render_bottom_splitter(cx); - let right_width = settings::get(cx).right_tool_window_width; + let right_width = settings::get(cx) + .right_tool_window_width + .clamp(240.0, 600.0); + let right_panel = self.right_tool.map(|tool| match tool { + RightToolView::Notifications => self.notifications.clone().into_any_element(), + RightToolView::Maven => self.maven.clone().into_any_element(), + RightToolView::Extensions => self.extensions.clone().into_any_element(), + }); + let mut center = h_resizable("workbench-center") + .on_resize(|state, _, cx| { + if state.read(cx).sizes().len() < 2 { + return; + } + if let Some(width) = state.read(cx).sizes().last() { + let w = width.as_f32().clamp(240.0, 600.0); + settings::update(cx, |s| s.right_tool_window_width = w); + } + }) + .child( + resizable_panel().child( + div() + .flex_1() + .h_full() + .min_w_0() + .bg(ThemeColors::background()) + .child(self.editor.clone()), + ), + ); + if let Some(panel) = right_panel { + center = center.child( + resizable_panel() + .size(px(right_width)) + .size_range(px(240.0)..px(600.0)) + .flex_none() + .child( + div() + .h_full() + .w_full() + .border_l_1() + .border_color(ThemeColors::border()) + .child(panel), + ), + ); + } v_flex() .size_full() .bg(ThemeColors::background()) @@ -1713,32 +1769,7 @@ impl WorkbenchView { ) .child(self.render_sidebar_splitter(cx)) }) - .child( - div() - .flex_1() - .h_full() - .min_w_0() - .bg(ThemeColors::background()) - .child(self.editor.clone()), - ) - .when_some(self.right_tool, |layout, tool| { - let panel = match tool { - RightToolView::Notifications => { - self.notifications.clone().into_any_element() - } - RightToolView::Maven => self.maven.clone().into_any_element(), - RightToolView::Extensions => self.extensions.clone().into_any_element(), - }; - layout.child( - div() - .w(px(right_width)) - .h_full() - .flex_shrink_0() - .border_l_1() - .border_color(ThemeColors::border()) - .child(panel), - ) - }) + .child(div().flex_1().h_full().min_w_0().child(center)) .child(self.plugin_rail.clone()), ) .when(bottom_visible, |layout| layout.child(bottom_splitter)) From 18c88ac9838e435d3383266cacdc90023865ef19 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 00:16:31 +0800 Subject: [PATCH 32/59] =?UTF-8?q?feat(linux):=20=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8=E6=A0=87=E7=AD=BE=E9=A1=B5=E5=8F=B3=E9=94=AE=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=EF=BC=8C=E5=AF=B9=E9=BD=90=20Tauri=20TabContextMenu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 关闭/关闭其他/关闭到右侧/关闭全部(指定索引版方法) - 复制路径/相对路径/文件管理器显示/终端打开/重新加载 - 固定/拆分/锁定组暂不适用(无分屏与固定态) --- linux/src/i18n.rs | 10 ++ linux/src/workbench/editor.rs | 119 ++++++++++++++++++- linux/src/workbench/mod.rs | 1 + linux/src/workbench/tab_menu.rs | 197 ++++++++++++++++++++++++++++++++ 4 files changed, 326 insertions(+), 1 deletion(-) create mode 100644 linux/src/workbench/tab_menu.rs diff --git a/linux/src/i18n.rs b/linux/src/i18n.rs index 15572e002..145231428 100644 --- a/linux/src/i18n.rs +++ b/linux/src/i18n.rs @@ -214,6 +214,16 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { "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"), + "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"), diff --git a/linux/src/workbench/editor.rs b/linux/src/workbench/editor.rs index d2fcecd80..abad408d0 100644 --- a/linux/src/workbench/editor.rs +++ b/linux/src/workbench/editor.rs @@ -1,10 +1,11 @@ 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, FontWeight, + div, px, relative, AppContext as _, ClipboardItem, Context, Entity, EventEmitter, FontWeight, InteractiveElement as _, IntoElement, ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Subscription, Window, }; @@ -55,6 +56,8 @@ pub struct EditorView { redo_stack: Vec, /// 已关闭标签(标签快照,原索引),后进先出供恢复。 closed_stack: Vec<(EditorTab, usize)>, + /// 右键菜单目标标签(UI 层读写,方法层不消费)。 + pub context_menu_tab: Option, } impl EditorView { @@ -92,6 +95,7 @@ impl EditorView { undo_stack: Vec::new(), redo_stack: Vec::new(), closed_stack: Vec::new(), + context_menu_tab: None, } } @@ -650,6 +654,91 @@ impl EditorView { 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 mut kept = None; + for (index, tab) in self.tabs.drain(..).enumerate() { + if index == idx { + kept = Some(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); + } + if let Some(tab) = kept { + self.tabs.push(tab); + self.active_tab_index = Some(0); + } else { + self.active_tab_index = None; + } + 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; + } + for index in idx + 1..self.tabs.len() { + 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); + } + self.tabs.truncate(idx + 1); + if let Some(active) = self.active_tab_index { + if active > idx { + self.active_tab_index = Some(idx); + } + } + self.sync_needed = true; + 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 { @@ -822,6 +911,14 @@ impl EditorView { } } +/// 编辑器标签派发的事件(右键菜单动作由 UI 层触发,父视图订阅处理)。 +#[derive(Debug, Clone)] +pub enum EditorTabEvent { + OpenInTerminal { dir: String }, +} + +impl EventEmitter for EditorView {} + /// 行注释风格:行前缀或行级块包裹。 /// 行注释风格:行前缀或行级块包裹。 @@ -1008,6 +1105,26 @@ impl Render for EditorView { .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(crate::workbench::tab_menu::tab_context_menu( + cx.entity(), + idx, + )) })), ) .when_some(active_tab.as_ref(), |this, tab| { diff --git a/linux/src/workbench/mod.rs b/linux/src/workbench/mod.rs index d8af16c5f..d0ee70fd1 100644 --- a/linux/src/workbench/mod.rs +++ b/linux/src/workbench/mod.rs @@ -14,6 +14,7 @@ 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; diff --git a/linux/src/workbench/tab_menu.rs b/linux/src/workbench/tab_menu.rs new file mode 100644 index 000000000..466e042ce --- /dev/null +++ b/linux/src/workbench/tab_menu.rs @@ -0,0 +1,197 @@ +//! 标签页右键菜单:Windows `tab-context-menu.tsx` 按键帽外子集。 +//! +//! 顺序固定为关闭/关闭其他/关闭到右侧/关闭全部|复制路径/复制相对路径/ +//! 在文件管理器中显示/在终端中打开/重新加载。文案经 +//! `crate::i18n::menu_text` 取 `tabs.*` 与 `files.*`,缺 key 回退为空。 +//! 触发与显隐走上游 `ContextMenuExt::context_menu`(右键冒泡打开,点选或点外 +//! 自动关闭,浮层 absolute 不占布局);`EditorView::context_menu_tab` 只做 +//! “右键目标”记账,右键按下时记 `idx`,选中动作与点外关闭时清 `None`。 + +use gpui_kit::assets::IconName; +use gpui_kit::component::h_flex; +use gpui_kit::component::menu::{PopupMenu, PopupMenuItem}; +use gpui_kit::{div, px, ClipboardItem, Context, Entity, ParentElement as _, Styled as _, Window}; + +use super::editor::{EditorTabEvent, EditorView}; +use crate::theme::ThemeColors; + +/// 右键菜单构造器:`view` 与 `idx` 以 owned 方式 move 进 `Fn` 闭包 +/// (`context_menu` 的 builder 要求 `Fn + 'static`,每次打开时调用)。 +pub fn tab_context_menu( + view: Entity, + idx: usize, +) -> impl Fn(PopupMenu, &mut Window, &mut Context) -> PopupMenu + 'static { + move |menu, _window, cx| build_tab_menu(menu, cx, &view, idx) +} + +/// 取绝对路径父目录(终端打开与文件管理器显示共用)。 +fn parent_dir(abs: &str) -> &str { + abs.rsplit_once('/').map(|(dir, _)| dir).unwrap_or(abs) +} + +/// 在文件管理器中显示所在目录(Linux-frame:`xdg-open`,失败忽略)。 +fn reveal_in_file_manager(dir: &str) { + let _ = std::process::Command::new("xdg-open").arg(dir).spawn(); +} + +fn build_tab_menu( + mut menu: PopupMenu, + cx: &mut Context, + view: &Entity, + idx: usize, +) -> PopupMenu { + // ---- 关闭组(Windows `close/close-others/close-right/close-all`) ---- + // 关闭带右侧 `Ctrl+W` 展示文本,用 element 自定义行(仿 `toolbar.rs`)。 + let v = view.clone(); + let close_label = crate::i18n::menu_text(cx, "tabs.close").to_string(); + menu = menu.item( + PopupMenuItem::element(move |_window, _cx| { + let label = close_label.clone(); + h_flex() + .w_full() + .items_center() + .gap_2() + .child( + div() + .flex_1() + .text_xs() + .text_color(ThemeColors::foreground()) + .child(label), + ) + .child( + div() + .text_xs() + .text_color(ThemeColors::subtle_foreground()) + .child("Ctrl+W"), + ) + }) + .icon(IconName::Close) + .on_click(move |_, _, cx| { + v.update(cx, |this, cx| { + this.close_tab(idx, cx); + this.context_menu_tab = None; + cx.notify(); + }); + }), + ); + + let v = view.clone(); + let close_others_label = crate::i18n::menu_text(cx, "tabs.closeOthers").to_string(); + menu = menu.item( + PopupMenuItem::new(close_others_label).on_click(move |_, _, cx| { + v.update(cx, |this, cx| { + this.close_others_at(idx, cx); + this.context_menu_tab = None; + cx.notify(); + }); + }), + ); + + let v = view.clone(); + let close_right_label = crate::i18n::menu_text(cx, "tabs.closeToRight").to_string(); + menu = menu.item( + PopupMenuItem::new(close_right_label).on_click(move |_, _, cx| { + v.update(cx, |this, cx| { + this.close_to_right_at(idx, cx); + this.context_menu_tab = None; + cx.notify(); + }); + }), + ); + + let v = view.clone(); + let close_all_label = crate::i18n::menu_text(cx, "tabs.closeAll").to_string(); + menu = menu.item( + PopupMenuItem::new(close_all_label).on_click(move |_, _, cx| { + v.update(cx, |this, cx| { + this.close_all_tabs(cx); + this.context_menu_tab = None; + cx.notify(); + }); + }), + ); + + menu = menu.separator(); + + // ---- 路径组(复制路径用绝对路径,相对路径用 `tab.path` 原样) ---- + let v = view.clone(); + let copy_path_label = crate::i18n::menu_text(cx, "files.copyPath").to_string(); + menu = menu.item( + PopupMenuItem::new(copy_path_label) + .icon(IconName::Copy) + .on_click(move |_, _, cx| { + v.update(cx, |this, cx| { + if let Some(abs) = this.tab_abs_path(idx) { + cx.write_to_clipboard(ClipboardItem::new_string(abs)); + } + this.context_menu_tab = None; + cx.notify(); + }); + }), + ); + + let v = view.clone(); + let copy_rel_label = crate::i18n::menu_text(cx, "files.copyRelativePath").to_string(); + menu = menu.item( + PopupMenuItem::new(copy_rel_label) + .icon(IconName::Copy) + .on_click(move |_, _, cx| { + v.update(cx, |this, cx| { + if let Some(rel) = this.tabs.get(idx).map(|tab| tab.path.clone()) { + cx.write_to_clipboard(ClipboardItem::new_string(rel)); + } + this.context_menu_tab = None; + cx.notify(); + }); + }), + ); + + let v = view.clone(); + let reveal_label = crate::i18n::menu_text(cx, "files.reveal").to_string(); + menu = menu.item( + PopupMenuItem::new(reveal_label) + .icon(IconName::FolderOpen) + .on_click(move |_, _, cx| { + v.update(cx, |this, cx| { + if let Some(abs) = this.tab_abs_path(idx) { + reveal_in_file_manager(parent_dir(&abs)); + } + this.context_menu_tab = None; + cx.notify(); + }); + }), + ); + + let v = view.clone(); + let terminal_label = crate::i18n::menu_text(cx, "files.openInTerminal").to_string(); + menu = menu.item( + PopupMenuItem::new(terminal_label) + .icon(IconName::Terminal) + .on_click(move |_, _, cx| { + v.update(cx, |this, cx| { + if let Some(abs) = this.tab_abs_path(idx) { + let dir = parent_dir(&abs).to_string(); + cx.emit(EditorTabEvent::OpenInTerminal { dir }); + } + this.context_menu_tab = None; + cx.notify(); + }); + }), + ); + + let v = view.clone(); + let reload_label = crate::i18n::menu_text(cx, "tabs.reload").to_string(); + menu = menu.item( + PopupMenuItem::new(reload_label) + .icon(IconName::RotateCw) + .on_click(move |_, _, cx| { + v.update(cx, |this, cx| { + this.reload_tab(idx, cx); + this.context_menu_tab = None; + cx.notify(); + }); + }), + ); + + menu +} From d0e0eb9461bb963f3d048cd7c51b611212a92a76 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 00:17:54 +0800 Subject: [PATCH 33/59] =?UTF-8?q?fix(linux):=20=E5=8E=BB=E6=8E=89=20tab=5F?= =?UTF-8?q?menu=20=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84=20px=20=E5=AF=BC?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- linux/src/workbench/tab_menu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux/src/workbench/tab_menu.rs b/linux/src/workbench/tab_menu.rs index 466e042ce..e13142cd2 100644 --- a/linux/src/workbench/tab_menu.rs +++ b/linux/src/workbench/tab_menu.rs @@ -10,7 +10,7 @@ use gpui_kit::assets::IconName; use gpui_kit::component::h_flex; use gpui_kit::component::menu::{PopupMenu, PopupMenuItem}; -use gpui_kit::{div, px, ClipboardItem, Context, Entity, ParentElement as _, Styled as _, Window}; +use gpui_kit::{div, ClipboardItem, Context, Entity, ParentElement as _, Styled as _, Window}; use super::editor::{EditorTabEvent, EditorView}; use crate::theme::ThemeColors; From 37868f17d57ff7287461ac549fa05a945102200e Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 00:23:27 +0800 Subject: [PATCH 34/59] =?UTF-8?q?fix(linux):=20=E5=BA=95=E9=83=A8=E7=BB=88?= =?UTF-8?q?=E7=AB=AF=E6=94=B6=E8=BF=9B=E7=BC=96=E8=BE=91=E5=8C=BA=E5=88=97?= =?UTF-8?q?=E5=AE=BD=EF=BC=8C=E5=AF=B9=E9=BD=90=20Tauri=20editor=20?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 底部 splitter+panel 移入中间列 v_flex,编辑器上、终端下 - 右侧工具窗移出到底部范围外,终端不再横跨全宽 --- linux/src/workbench/view.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/linux/src/workbench/view.rs b/linux/src/workbench/view.rs index 8981d4504..eec1117eb 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -1723,12 +1723,22 @@ impl WorkbenchView { }) .child( resizable_panel().child( - div() - .flex_1() + v_flex() .h_full() + .w_full() .min_w_0() - .bg(ThemeColors::background()) - .child(self.editor.clone()), + .child( + div() + .flex_1() + .w_full() + .min_h_0() + .bg(ThemeColors::background()) + .child(self.editor.clone()), + ) + .when(bottom_visible, |layout| layout.child(bottom_splitter)) + .when(bottom_visible, |layout| { + layout.child(self.bottom_panel.clone()) + }), ), ); if let Some(panel) = right_panel { @@ -1772,10 +1782,6 @@ impl WorkbenchView { .child(div().flex_1().h_full().min_w_0().child(center)) .child(self.plugin_rail.clone()), ) - .when(bottom_visible, |layout| layout.child(bottom_splitter)) - .when(bottom_visible, |layout| { - layout.child(self.bottom_panel.clone()) - }) .when(show_status_bar, |layout| { layout.child(self.status_bar.clone()) }) From abe7491ed42ee493886806eef75c751842970fc5 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 00:32:24 +0800 Subject: [PATCH 35/59] =?UTF-8?q?fix(linux):=20=E8=BF=90=E8=A1=8C=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E8=A1=8C=E4=B8=BA=E5=AF=B9=E9=BD=90=20Tauri?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 工具栏 Run/Debug 走底部 Run 页受管进程,不再发终端假命令 - Stop 杀 Run/Maven 子进程;Maven 目标走新增底部 Maven 页 - 底部新增 Maven 页签(launchPlan 参数 + $ mvn 首行输出) - 左侧 maven 项改由 Maven 任务点亮,终端命令不再误触 --- linux/src/workbench/bottom_panel.rs | 308 +++++++++++++++++++++++++++- linux/src/workbench/view.rs | 52 +++-- 2 files changed, 344 insertions(+), 16 deletions(-) diff --git a/linux/src/workbench/bottom_panel.rs b/linux/src/workbench/bottom_panel.rs index d24063ac0..6838914f5 100644 --- a/linux/src/workbench/bottom_panel.rs +++ b/linux/src/workbench/bottom_panel.rs @@ -66,6 +66,7 @@ const MAX_GIT_LOG_ENTRIES: usize = 50; pub enum BottomTab { Terminal, Run, + Maven, Diagnostics, GitLog, } @@ -100,10 +101,20 @@ pub struct BottomPanelView { pub(crate) run_output: Vec, /// 是否有进程在跑。 pub(crate) run_running: bool, + /// 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, /// 在跑子进程句柄:停止按钮经 `kill` 停,运行线程经 `try_wait` 短锁轮询收割。 run_child: Arc>>, + /// Maven 任务子进程句柄(与 Run 共用停止语义,分开存放可各自启停)。 + maven_child: Arc>>, + /// Maven 启动序号,丢弃过期 `launchPlan` 结果。 + maven_seq: u64, /// reload 序号,丢弃过期探测结果。 run_seq: u64, /// 最近一次加载的 Git 提交记录。 @@ -130,6 +141,11 @@ impl BottomPanelView { selected_run_config: None, run_output: Vec::new(), run_running: false, + maven_title: None, + maven_output: Vec::new(), + maven_running: false, + maven_child: Arc::new(Mutex::new(None)), + maven_seq: 0, client: CoreClient::new(), run_child: Arc::new(Mutex::new(None)), run_seq: 0, @@ -339,15 +355,144 @@ impl BottomPanelView { .detach(); } - /// 停止在跑进程(只 kill 直接子进程;mvn 拉起的 java 孙进程不在此列)。 + /// 停止在跑进程(Run 与 Maven 各自 kill;mvn 拉起的 java 孙进程不在此列)。 pub fn stop_running(&mut self, cx: &mut Context) { + let mut stopped = false; if let Ok(mut slot) = self.run_child.lock() { if let Some(child) = slot.as_mut() { let _ = child.kill(); + stopped = true; } } - push_run_line(&mut self.run_output, "已发送停止信号…".to_string()); + if let Ok(mut slot) = self.maven_child.lock() { + if let Some(child) = slot.as_mut() { + let _ = child.kill(); + stopped = true; + } + } + if self.run_running { + push_run_line(&mut self.run_output, "已发送停止信号…".to_string()); + stopped = true; + } + if self.maven_running { + push_run_line(&mut self.maven_output, "已发送停止信号…".to_string()); + 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 目标:切 Maven 页 + 经 core `maven.launchPlan` 拿确定性参数 + ///(payload `{root, context: {version: 1, reactorPath: "."}, module, goals}`), + /// 起受管进程并把输出泵入 Maven 页(首行 `$ mvn …`,对齐 Tauri Maven 页)。 + /// 已有任务在跑时先停掉(对齐 Tauri 停掉上一个 session)。 + pub fn run_maven_goal(&mut self, pom_path: &str, goal: &str, cx: &mut Context) { + let goal = goal.trim().to_string(); + if goal.is_empty() { + return; + } + if self.maven_running { + if let Ok(mut slot) = self.maven_child.lock() { + if let Some(child) = slot.as_mut() { + let _ = child.kill(); + } + } + } + self.maven_seq += 1; + let seq = self.maven_seq; + let title = format!( + "{goal} · {}", + maven_display_pom(&self.working_dir, pom_path) + ); + self.active_tab = BottomTab::Maven; + self.is_collapsed = false; + self.maven_title = Some(title.clone()); + 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 child_slot = self.maven_child.clone(); + cx.spawn(async move |this, cx| { + let plan = client + .execute::( + &cx, + "maven.launchPlan", + serde_json::json!({ + "root": root, + "context": { "version": 1, "reactorPath": "." }, + "module": module, + "goals": [goal], + }), + ) + .await + .ok(); + let (program, args, cwd) = maven_plan_to_step(plan.as_ref(), &root, &goal); + let display = format!("$ {} {}", program, args.join(" ")); + let started = this.update(cx, |view, cx| { + if view.maven_seq != seq { + return false; + } + push_run_line(&mut view.maven_output, display.clone()); + view.record_run(&display, cx); + cx.notify(); + true + }); + if !matches!(started, Ok(true)) { + return; + } + let steps = vec![RunStep { program, args, cwd }]; + let (tx, rx) = mpsc::channel::(); + std::thread::spawn(move || run_steps_blocking(steps, child_slot, tx)); + let rx = Arc::new(Mutex::new(rx)); + loop { + let slot = rx.clone(); + let next = cx + .background_executor() + .spawn(async move { + let guard = slot.lock().ok()?; + guard.recv().ok() + }) + .await; + match next { + Some(line) => { + // `run_steps_blocking` 首行会再发一次 `$ …`,Maven 页已有 + // 展示首行,跳过重复(对齐 Tauri 单首行)。 + let skip = this + .update(cx, |view, cx| { + let duplicate = line.trim_start().starts_with("$ ") + && !view.maven_output.is_empty(); + if !duplicate { + push_run_line(&mut view.maven_output, line); + } + cx.notify(); + view.maven_seq == seq + }) + .unwrap_or(false); + if !skip { + break; + } + } + None => break, + } + } + let _ = this.update(cx, |view, cx| { + if view.maven_seq == seq { + view.maven_running = false; + } + cx.notify(); + }); + }) + .detach(); } /// 当前选中项(无选中或已失效时回退首项)。 @@ -701,6 +846,85 @@ impl BottomPanelView { .into_any_element() } + /// Maven 面板:对齐 Tauri Maven 页——顶部任务标题+运行状态+停止/清空, + /// 输出区首行 `$ mvn …`,流式追加进程输出。 + fn render_maven_panel(&self, cx: &mut Context) -> AnyElement { + let status = if self.maven_running { + run_ui_text(cx, "运行中…", "Running…") + } else if let Some(title) = self.maven_title.clone() { + title + } else { + run_ui_text(cx, "尚未运行 Maven 目标", "No Maven goal has run yet") + }; + 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(run_ui_text(cx, "暂无输出", "No output")) + .into_any_element()] + } else { + self.maven_output[start..] + .iter() + .map(|line| div().child(line.clone()).into_any_element()) + .collect() + }; + v_flex() + .size_full() + .child( + h_flex() + .flex_shrink_0() + .h(px(30.0)) + .w_full() + .items_center() + .gap_2() + .px_3() + .border_b_1() + .border_color(ThemeColors::border()) + .child( + div() + .flex_1() + .truncate() + .text_xs() + .text_color(ThemeColors::text_primary()) + .child(status), + ) + .child( + Button::new("maven-stop") + .small() + .primary() + .disabled(!self.maven_running) + .label(run_ui_text(cx, "停止", "Stop")) + .on_click(cx.listener(|this, _event, _window, cx| { + this.stop_running(cx); + })), + ) + .child( + Button::new("maven-clear-output") + .small() + .ghost() + .label(run_ui_text(cx, "清空输出", "Clear")) + .on_click(cx.listener(|this, _event, _window, cx| { + this.maven_output.clear(); + cx.notify(); + })), + ), + ) + .child( + 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() { @@ -815,6 +1039,14 @@ impl Render for BottomPanelView { BottomTab::Run, cx, )) + .child(self.render_tab_button( + "tab-maven", + IconName::Box, + crate::i18n::menu_text(cx, "maven.title").to_string(), + self.active_tab == BottomTab::Maven, + BottomTab::Maven, + cx, + )) .child(self.render_tab_button( "tab-diagnostics", IconName::TriangleAlert, @@ -857,6 +1089,10 @@ impl Render for BottomPanelView { this.run_output.clear(); cx.notify(); } + BottomTab::Maven => { + this.maven_output.clear(); + cx.notify(); + } BottomTab::Diagnostics => { this.diagnostics.clear(); cx.notify(); @@ -888,6 +1124,7 @@ impl Render for BottomPanelView { .child(self.terminal.clone()) .into_any_element(), BottomTab::Run => self.render_run_panel(cx), + BottomTab::Maven => self.render_maven_panel(cx), BottomTab::Diagnostics => div() .size_full() .p_3() @@ -1236,6 +1473,73 @@ fn plan_to_steps( fallback_run_steps(item, root) } +/// Maven 页展示用的 pom 路径:相对工作区,根 pom 显示 `pom.xml`。 +fn maven_display_pom(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()) +} + +/// pom 所在目录相对 root 的模块路径:根 pom 为 `.`,与 core 模块约定一致。 +fn maven_module_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 + } +} + +/// `maven.launchPlan`(`{executable.toolchain, arguments[], workingDirectory}`) +/// 转本地执行步骤;拿不到计划时退化为 `mvn `。 +fn maven_plan_to_step( + plan: Option<&serde_json::Value>, + root: &str, + goal: &str, +) -> (String, Vec, String) { + if let Some(plan) = plan { + let cwd = plan + .get("workingDirectory") + .and_then(|v| v.as_str()) + .unwrap_or("."); + let cwd = if cwd == "." || cwd.is_empty() { + root.to_string() + } else { + format!("{root}/{cwd}") + }; + let args = plan + .get("arguments") + .and_then(|a| a.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str().map(str::to_string)) + .collect::>() + }) + .filter(|args| !args.is_empty()); + if let Some(args) = args { + return (resolve_maven_executable(root), args, cwd); + } + } + ( + resolve_maven_executable(root), + vec![goal.to_string()], + root.to_string(), + ) +} + /// 退化执行规则:maven 用 mvn 跑 `compile exec:java`;npm 跑 `run dev`; /// java 单文件走 javac+java,否则直跑主类。 fn fallback_run_steps(item: &RunConfigItem, root: &str) -> Vec { diff --git a/linux/src/workbench/view.rs b/linux/src/workbench/view.rs index eec1117eb..0d83075da 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -344,10 +344,17 @@ impl WorkbenchView { ); // 4b. 订阅 Maven 视图事件(执行目标、关闭工具窗口) + // 对齐 Tauri:目标走底部 Maven 页受管进程,不进交互终端。 let sub_maven = cx.subscribe(&maven, |this, _maven, event: &MavenEvent, cx| match event { MavenEvent::RunGoal { pom_path, phase } => { - let cmd = format!("mvn -f \"{pom_path}\" {phase}"); - this.send_terminal_command(&cmd, cx); + let _ = this.bottom_panel.update(cx, |bp, cx| { + bp.run_maven_goal(pom_path, phase, cx); + }); + let has_run = this.bottom_panel.read(cx).has_maven_run(); + let _ = this.activity_rail.update(cx, |r, cx| { + r.set_has_maven_run(has_run, cx); + }); + cx.notify(); } MavenEvent::Close => { this.right_tool = None; @@ -402,13 +409,17 @@ impl WorkbenchView { this.open_quick_open(cx); } ToolbarEvent::Run => { - this.send_terminal_command("echo '[Lithe Run]' && cargo check", cx); + this.open_run_pane(cx); } ToolbarEvent::Debug => { - this.append_log("[Debug] Launching DAP debug session...", cx); + // Linux 无独立 DAP 面板:对齐 Tauri 可见行为,Debug 同样 + // 走底部受管进程(Run 页),不再只记一条假日志。 + this.open_run_pane(cx); } ToolbarEvent::Stop => { - this.append_log("[Stop] Session terminated by user.", cx); + let _ = this.bottom_panel.update(cx, |bp, cx| { + bp.stop_running(cx); + }); } ToolbarEvent::OpenSettings => { this.open_settings(cx); @@ -880,8 +891,8 @@ impl WorkbenchView { cx.notify(); } - /// 经底部终端发送命令:执行 + 计入运行历史 + 点亮左侧 maven 项 - ///(对齐 Tauri `toggleTerminalPane` 系 + `hasMavenRun` 挂载语义)。 + /// 经底部终端发送命令:只执行,不计入 maven 运行门控 + ///(对齐 Tauri:左侧 maven 项仅由 Maven 任务点亮)。 fn send_terminal_command(&mut self, cmd: &str, cx: &mut Context) { let cmd = cmd.to_string(); let _ = self.bottom_panel.update(cx, |bp, cx| { @@ -891,9 +902,15 @@ impl WorkbenchView { }); bp.record_run(&cmd, cx); }); - let has_run = self.bottom_panel.read(cx).has_run_history(); - let _ = self.activity_rail.update(cx, |r, cx| { - r.set_has_maven_run(has_run, cx); + cx.notify(); + } + + /// 打开底部 Run 页并运行选中配置:对齐 Tauri `openRunDecisionPane` + + /// `runConfiguration`(页签可见 + 受管进程,不进交互终端)。 + fn open_run_pane(&mut self, cx: &mut Context) { + let _ = self.bottom_panel.update(cx, |bp, cx| { + bp.set_tab(BottomTab::Run, cx); + bp.run_selected_config(cx); }); cx.notify(); } @@ -1176,10 +1193,15 @@ impl WorkbenchView { }); } "workbench.run" | "run.run" | "run.start" => { - self.send_terminal_command("cargo check", cx); + self.open_run_pane(cx); } "workbench.debug" | "run.debug" => { - self.append_log("[Debug] Launching DAP debug session...", cx); + self.open_run_pane(cx); + } + "workbench.stop" | "run.stop" => { + let _ = self.bottom_panel.update(cx, |bp, cx| { + bp.stop_running(cx); + }); } "workbench.clear_terminal" | "terminal.clear" => { let _ = self.bottom_panel.update(cx, |bp, cx| { @@ -1492,11 +1514,13 @@ fn sidebar_tab_for(view_id: &str) -> Option { } } -/// 活动栏底部项 id → 底部面板标签(对齐 Tauri `BottomPaneTab`)。 +/// 活动栏底部项 id → 底部面板标签(对齐 Tauri `BottomPaneTab`: +/// `run` 与 `maven` 是两个独立页)。 fn bottom_tab_for(pane_id: &str) -> Option { match pane_id { "terminal" => Some(BottomTab::Terminal), - "run" | "maven" => Some(BottomTab::Run), + "run" => Some(BottomTab::Run), + "maven" => Some(BottomTab::Maven), "diagnostics" => Some(BottomTab::Diagnostics), "gitLog" => Some(BottomTab::GitLog), _ => None, From b1dee2a757a2e1cdebca3f21e5b0d420268cf9ee Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 00:39:47 +0800 Subject: [PATCH 36/59] =?UTF-8?q?feat(linux):=20=E5=9B=BA=E5=AE=9A?= =?UTF-8?q?=E6=A0=87=E7=AD=BE=E9=A1=B5=E4=B8=8E=E7=BC=96=E8=BE=91=E5=99=A8?= =?UTF-8?q?=E5=88=86=E5=B1=8F=EF=BC=8C=E5=AF=B9=E9=BD=90=20Tauri?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 固定标签:is_pinned 置顶组,批量关闭跳过,菜单固定项 - 窗格树 panes:任意嵌套左右/上下拆分、关闭合并、比例拖拽 - 锁定窗格:新文件路由自动跳过锁定窗格 - 菜单补齐拆分/锁定项 --- linux/src/i18n.rs | 7 + linux/src/workbench/editor.rs | 189 +++++++++++---- linux/src/workbench/mod.rs | 1 + linux/src/workbench/panes.rs | 357 +++++++++++++++++++++++++++ linux/src/workbench/tab_menu.rs | 105 +++++++- linux/src/workbench/view.rs | 414 ++++++++++++++++++++++++++------ 6 files changed, 952 insertions(+), 121 deletions(-) create mode 100644 linux/src/workbench/panes.rs diff --git a/linux/src/i18n.rs b/linux/src/i18n.rs index 145231428..b4a62c637 100644 --- a/linux/src/i18n.rs +++ b/linux/src/i18n.rs @@ -220,6 +220,13 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { "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"), diff --git a/linux/src/workbench/editor.rs b/linux/src/workbench/editor.rs index abad408d0..32b542776 100644 --- a/linux/src/workbench/editor.rs +++ b/linux/src/workbench/editor.rs @@ -10,6 +10,11 @@ use gpui_kit::{ 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; @@ -20,6 +25,8 @@ pub struct EditorTab { pub title: String, pub content: String, pub is_dirty: bool, + /// 固定标签:批量关闭跳过,固定组永远排在标签栏前段。 + pub is_pinned: bool, #[allow(dead_code)] pub cursor_line: usize, #[allow(dead_code)] @@ -58,6 +65,14 @@ pub struct EditorView { 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 { @@ -96,6 +111,10 @@ impl EditorView { 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, } } @@ -124,6 +143,7 @@ impl EditorView { title, content, is_dirty: false, + is_pinned: false, cursor_line: 1, cursor_col: 1, }); @@ -533,25 +553,35 @@ impl EditorView { } } - /// 关闭全部标签(逐个进恢复栈)。 + /// 关闭全部标签(逐个进恢复栈;固定标签保留)。 pub fn close_all_tabs(&mut self, cx: &mut Context) { if self.tabs.is_empty() { return; } - for (index, tab) in self.tabs.iter().enumerate() { - self.closed_stack.push((tab.clone(), index)); + 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.clear(); - self.active_tab_index = None; + 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; @@ -559,10 +589,11 @@ impl EditorView { if active >= self.tabs.len() { return; } - let mut kept = None; + 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 { - kept = Some(tab); + if index == active || tab.is_pinned { + kept.push((index, tab)); } else { self.closed_stack.push((tab, index)); } @@ -571,17 +602,16 @@ impl EditorView { let overflow = self.closed_stack.len() - 30; self.closed_stack.drain(..overflow); } - if let Some(tab) = kept { - self.tabs.push(tab); - self.active_tab_index = Some(0); - } else { - self.active_tab_index = None; - } + // 固定组在前并保持原相对顺序。 + 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`)的标签。 + /// 关闭全部未修改(`is_dirty == false`)的标签(固定标签保留)。 pub fn close_saved_tabs(&mut self, cx: &mut Context) { if self.tabs.is_empty() { return; @@ -592,7 +622,7 @@ impl EditorView { .map(|tab| tab.path.clone()); let mut kept = Vec::new(); for (index, tab) in self.tabs.drain(..).enumerate() { - if tab.is_dirty { + if tab.is_dirty || tab.is_pinned { kept.push(tab); } else { self.closed_stack.push((tab, index)); @@ -609,7 +639,7 @@ impl EditorView { cx.notify(); } - /// 关闭活动标签左侧的全部标签。 + /// 关闭活动标签左侧的全部标签(固定标签保留在前段)。 pub fn close_tabs_to_left(&mut self, cx: &mut Context) { let Some(active) = self.active_tab_index else { return; @@ -617,8 +647,12 @@ impl EditorView { if active == 0 || active >= self.tabs.len() { return; } - for index in 0..active { - if let Some(tab) = self.tabs.get(index).cloned() { + 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)); } } @@ -626,13 +660,16 @@ impl EditorView { let overflow = self.closed_stack.len() - 30; self.closed_stack.drain(..overflow); } - self.tabs.drain(..active); - self.active_tab_index = Some(0); + 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; @@ -640,29 +677,33 @@ impl EditorView { if active >= self.tabs.len() || active + 1 >= self.tabs.len() { return; } - for index in active + 1..self.tabs.len() { - if let Some(tab) = self.tabs.get(index).cloned() { - self.closed_stack.push((tab, index)); + 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.truncate(active + 1); + self.tabs.extend(pinned_right); self.sync_needed = true; cx.notify(); } - /// 关闭除 `idx` 外的全部标签(`close_other_tabs` 的指定索引版,供右键菜单调用)。 + /// 关闭除 `idx` 外的全部标签(`close_other_tabs` 的指定索引版,供右键菜单调用;固定标签保留,固定组在前)。 pub fn close_others_at(&mut self, idx: usize, cx: &mut Context) { if idx >= self.tabs.len() { return; } - let mut kept = None; + 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 { - kept = Some(tab); + if index == idx || tab.is_pinned { + kept.push((index, tab)); } else { self.closed_stack.push((tab, index)); } @@ -671,40 +712,77 @@ impl EditorView { let overflow = self.closed_stack.len() - 30; self.closed_stack.drain(..overflow); } - if let Some(tab) = kept { - self.tabs.push(tab); - self.active_tab_index = Some(0); - } else { - self.active_tab_index = None; - } + // 固定组在前并保持原相对顺序;`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` 的指定索引版,供右键菜单调用)。 + /// 关闭 `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; } - for index in idx + 1..self.tabs.len() { - if let Some(tab) = self.tabs.get(index).cloned() { - self.closed_stack.push((tab, index)); + 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.truncate(idx + 1); + self.tabs.extend(pinned_right); if let Some(active) = self.active_tab_index { if active > idx { - self.active_tab_index = Some(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 { @@ -1121,10 +1199,27 @@ impl Render for EditorView { cx.notify(); } })) - .context_menu(crate::workbench::tab_menu::tab_context_menu( - cx.entity(), - idx, - )) + .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| { diff --git a/linux/src/workbench/mod.rs b/linux/src/workbench/mod.rs index d0ee70fd1..3ae503ed6 100644 --- a/linux/src/workbench/mod.rs +++ b/linux/src/workbench/mod.rs @@ -8,6 +8,7 @@ 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 search_everywhere; 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/tab_menu.rs b/linux/src/workbench/tab_menu.rs index e13142cd2..656403ada 100644 --- a/linux/src/workbench/tab_menu.rs +++ b/linux/src/workbench/tab_menu.rs @@ -1,27 +1,55 @@ //! 标签页右键菜单:Windows `tab-context-menu.tsx` 按键帽外子集。 //! -//! 顺序固定为关闭/关闭其他/关闭到右侧/关闭全部|复制路径/复制相对路径/ -//! 在文件管理器中显示/在终端中打开/重新加载。文案经 +//! 顺序固定为固定/取消固定|向右拆分/向下拆分|锁定组| +//! 关闭/关闭其他/关闭到右侧/关闭全部| +//! 复制路径/复制相对路径/在文件管理器中显示/在终端中打开/重新加载。文案经 //! `crate::i18n::menu_text` 取 `tabs.*` 与 `files.*`,缺 key 回退为空。 //! 触发与显隐走上游 `ContextMenuExt::context_menu`(右键冒泡打开,点选或点外 //! 自动关闭,浮层 absolute 不占布局);`EditorView::context_menu_tab` 只做 //! “右键目标”记账,右键按下时记 `idx`,选中动作与点外关闭时清 `None`。 +use std::rc::Rc; + use gpui_kit::assets::IconName; use gpui_kit::component::h_flex; use gpui_kit::component::menu::{PopupMenu, PopupMenuItem}; -use gpui_kit::{div, ClipboardItem, Context, Entity, ParentElement as _, Styled as _, Window}; +use gpui_kit::{div, App, ClipboardItem, Context, Entity, ParentElement as _, Styled as _, Window}; use super::editor::{EditorTabEvent, EditorView}; +use super::panes::{PaneId, SplitDir}; use crate::theme::ThemeColors; +/// 拆分回调:`Fn(pane_id, dir, window, cx)`,由工作台注入(内部经 workbench +/// entity 回写 `split_pane`),菜单点击时触发(`window` 供新建编辑器用)。 +pub type SplitCallback = Rc; +/// 锁定回调:`Fn(pane_id, cx)`,由工作台注入(内部经 workbench entity 回写 +/// `toggle_lock`),菜单点击时触发。 +pub type ToggleLockCallback = Rc; + /// 右键菜单构造器:`view` 与 `idx` 以 owned 方式 move 进 `Fn` 闭包 /// (`context_menu` 的 builder 要求 `Fn + 'static`,每次打开时调用)。 +/// `pane_id` 为标签所在窗格,`is_locked` 决定锁定项文案;拆分/锁定动作经 +/// `on_split` / `on_toggle_lock` 回工作台执行,本层不直接改窗格树。 pub fn tab_context_menu( view: Entity, idx: usize, + pane_id: PaneId, + is_locked: bool, + on_split: SplitCallback, + on_toggle_lock: ToggleLockCallback, ) -> impl Fn(PopupMenu, &mut Window, &mut Context) -> PopupMenu + 'static { - move |menu, _window, cx| build_tab_menu(menu, cx, &view, idx) + move |menu, _window, cx| { + build_tab_menu( + menu, + cx, + &view, + idx, + pane_id, + is_locked, + &on_split, + &on_toggle_lock, + ) + } } /// 取绝对路径父目录(终端打开与文件管理器显示共用)。 @@ -39,7 +67,76 @@ fn build_tab_menu( cx: &mut Context, view: &Entity, idx: usize, + pane_id: PaneId, + is_locked: bool, + on_split: &SplitCallback, + on_toggle_lock: &ToggleLockCallback, ) -> PopupMenu { + // ---- 固定组(Windows `pin` 首项,其后分隔线;`IconName` 无 Pin 变体故不用图标) ---- + let is_pinned = view.read(cx).tabs.get(idx).is_some_and(|tab| tab.is_pinned); + let v = view.clone(); + let pin_label = if is_pinned { + crate::i18n::menu_text(cx, "tabs.unpin").to_string() + } else { + crate::i18n::menu_text(cx, "tabs.pin").to_string() + }; + menu = menu.item(PopupMenuItem::new(pin_label).on_click(move |_, _, cx| { + v.update(cx, |this, cx| { + this.toggle_pin_at(idx, cx); + this.context_menu_tab = None; + cx.notify(); + }); + })); + + menu = menu.separator(); + + // ---- 拆分组(Windows `split-right/split-down`,其后分隔线) ---- + let cb = on_split.clone(); + let v = view.clone(); + let split_right_label = crate::i18n::menu_text(cx, "tabs.splitRight").to_string(); + menu = menu.item( + PopupMenuItem::new(split_right_label).on_click(move |_, window, cx| { + cb(pane_id, SplitDir::Horizontal, window, cx); + v.update(cx, |this, cx| { + this.context_menu_tab = None; + cx.notify(); + }); + }), + ); + + let cb = on_split.clone(); + let v = view.clone(); + let split_down_label = crate::i18n::menu_text(cx, "tabs.splitDown").to_string(); + menu = menu.item( + PopupMenuItem::new(split_down_label).on_click(move |_, window, cx| { + cb(pane_id, SplitDir::Vertical, window, cx); + v.update(cx, |this, cx| { + this.context_menu_tab = None; + cx.notify(); + }); + }), + ); + + menu = menu.separator(); + + // ---- 锁定组(Windows `toggle-editor-group-lock`,其后分隔线) ---- + let cb = on_toggle_lock.clone(); + let v = view.clone(); + let lock_label = if is_locked { + crate::i18n::menu_text(cx, "tabs.unlockEditorGroup").to_string() + } else { + crate::i18n::menu_text(cx, "tabs.lockEditorGroup").to_string() + }; + menu = menu.item(PopupMenuItem::new(lock_label).on_click(move |_, _, cx| { + cb(pane_id, cx); + v.update(cx, |this, cx| { + this.context_menu_tab = None; + cx.notify(); + }); + })); + + menu = menu.separator(); + // ---- 关闭组(Windows `close/close-others/close-right/close-all`) ---- // 关闭带右侧 `Ctrl+W` 展示文本,用 element 自定义行(仿 `toolbar.rs`)。 let v = view.clone(); diff --git a/linux/src/workbench/view.rs b/linux/src/workbench/view.rs index 0d83075da..570e61f77 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -3,15 +3,19 @@ //! 结构对齐 Tauri `MainLayout`:顶栏 / 多项目标签条 / 活动栏 + 侧边栏 + 编辑区 + //! 右侧插件活动栏 / 底部面板 / 状态栏;无项目时显示欢迎页,浮层由模态状态控制。 -use gpui_kit::component::resizable::{h_resizable, resizable_panel}; -use gpui_kit::component::{h_flex, v_flex}; +use gpui_kit::component::button::{Button, ButtonVariants as _}; +use gpui_kit::component::resizable::{h_resizable, resizable_panel, v_resizable, ResizableState}; +use gpui_kit::component::{h_flex, v_flex, Sizable as _}; use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::{ - div, px, AppContext as _, Context, Entity, FocusHandle, InteractiveElement as _, IntoElement, - KeyDownEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, - Render, Styled as _, Subscription, Window, + div, px, AnyElement, App, AppContext as _, Context, Entity, FocusHandle, + InteractiveElement as _, IntoElement, KeyDownEvent, MouseButton, MouseDownEvent, + MouseMoveEvent, MouseUpEvent, ParentElement as _, Render, Styled as _, Subscription, Window, }; +use std::collections::HashMap; +use std::rc::Rc; + use crate::core::CoreClient; use crate::settings; use crate::theme::ThemeColors; @@ -26,12 +30,14 @@ use crate::workbench::extensions_panel::{ExtensionsEvent, ExtensionsView}; use crate::workbench::go_to_line::{GoToLineEvent, GoToLineModal}; use crate::workbench::maven::{MavenEvent, MavenView}; use crate::workbench::notifications::{NotificationsEvent, NotificationsView}; +use crate::workbench::panes::{PaneId, PaneNode, PaneTree, SplitDir}; use crate::workbench::project_dialog::{ProjectDialog, ProjectDialogEvent, ProjectDialogMode}; use crate::workbench::quick_open::{QuickOpenEvent, QuickOpenModal}; use crate::workbench::search_everywhere::{SearchEverywhereEvent, SearchEverywhereModal}; use crate::workbench::settings_dialog::{SettingsCategory, SettingsDialog, SettingsEvent}; use crate::workbench::sidebar::{FileEntry, SidebarEvent, SidebarTab, SidebarView}; use crate::workbench::status_bar::{StatusBarEvent, StatusBarView}; +use crate::workbench::tab_menu::{SplitCallback, ToggleLockCallback}; use crate::workbench::toolbar::{ToolbarEvent, ToolbarView}; use crate::workbench::welcome_screen::{WelcomeEvent, WelcomeScreenView}; @@ -97,8 +103,10 @@ pub struct WorkbenchView { pending_goto_line: Option, /// 侧边栏面板 (Files / Git / Search) pub sidebar: Entity, - /// 主代码编辑器区 - pub editor: Entity, + /// 多窗格布局模型(`Split{Horizontal}` 左右并排,`{Vertical}` 上下堆叠) + pub pane_tree: PaneTree, + /// 各窗格编辑器实体(`PaneId` → `EditorView`) + pub pane_editors: HashMap>, /// 底部抽屉面板 (Terminal / Diagnostics,对齐 Tauri BottomPaneTab) pub bottom_panel: Entity, /// 底部状态栏 (Status Bar) @@ -141,7 +149,12 @@ impl WorkbenchView { let activity_rail = cx.new(|cx| ActivityRailView::new(cx)); let plugin_rail = cx.new(|_cx| PluginActivityRailView::new()); let sidebar = cx.new(|cx| SidebarView::new(root.clone(), cx)); + let pane_tree = PaneTree::new(); + let initial_pane = pane_tree.active().unwrap_or(0); let editor = cx.new(|cx| EditorView::new(root.clone(), window, cx)); + let mut pane_editors = HashMap::new(); + pane_editors.insert(initial_pane, editor.clone()); + Self::wire_pane_editor(&pane_tree, cx.entity(), initial_pane, &editor, cx); let bottom_panel = cx.new(|cx| BottomPanelView::new(root.clone(), cx)); let status_bar = cx.new(|_cx| StatusBarView::new()); let search_everywhere = cx.new(|cx| SearchEverywhereModal::new(cx)); @@ -177,9 +190,11 @@ impl WorkbenchView { }); } SidebarEvent::NewFile => { - let _ = this.editor.update(cx, |ed, cx| { - ed.open_file("untitled.txt".to_string(), String::new(), cx); - }); + if let Some(routed) = this.routed_editor() { + let _ = routed.update(cx, |ed, cx| { + ed.open_file("untitled.txt".to_string(), String::new(), cx); + }); + } let _ = status_bar_clone.update(cx, |sb, cx| { sb.set_file_info( Some("untitled.txt".to_string()), @@ -232,14 +247,18 @@ impl WorkbenchView { qo.set_files(file_list, cx); }); } - // 已打开标签页同步给快速打开置顶分组(对齐 Tauri `openBufferFiles`)。 - let open_files: Vec = this - .editor - .read(cx) - .tabs - .iter() - .map(|tab| tab.path.clone()) - .collect(); + // 已打开标签页同步给快速打开置顶分组(对齐 Tauri `openBufferFiles`, + // 多窗格取各窗格 tabs 去重并集)。 + let mut open_files: Vec = Vec::new(); + for leaf in this.pane_tree.leaves() { + if let Some(ed) = this.pane_editors.get(&leaf) { + for tab in ed.read(cx).tabs.iter() { + if !open_files.contains(&tab.path) { + open_files.push(tab.path.clone()); + } + } + } + } if !open_files.is_empty() { let _ = quick_open_sync.update(cx, |qo, cx| { qo.set_open_files(open_files, cx); @@ -368,21 +387,27 @@ impl WorkbenchView { &toolbar, |this, _toolbar, event: &ToolbarEvent, cx| match event { ToolbarEvent::NewFile => { - let _ = this.editor.update(cx, |ed, cx| { - ed.open_file("untitled.txt".to_string(), String::new(), cx); - }); + if let Some(active) = this.active_editor() { + let _ = active.update(cx, |ed, cx| { + ed.open_file("untitled.txt".to_string(), String::new(), cx); + }); + } } ToolbarEvent::Save => { - let _ = this.editor.update(cx, |ed, cx| { - ed.save_active(cx); - }); + if let Some(active) = this.active_editor() { + let _ = active.update(cx, |ed, cx| { + ed.save_active(cx); + }); + } } ToolbarEvent::CloseTab => { - let _ = this.editor.update(cx, |ed, cx| { - if let Some(idx) = ed.active_tab_index { - ed.close_tab(idx, cx); - } - }); + if let Some(active) = this.active_editor() { + let _ = active.update(cx, |ed, cx| { + if let Some(idx) = ed.active_tab_index { + ed.close_tab(idx, cx); + } + }); + } } ToolbarEvent::ToggleSidebar => { this.toggle_sidebar(cx); @@ -749,7 +774,8 @@ impl WorkbenchView { notifications, extensions, sidebar, - editor, + pane_tree, + pane_editors, bottom_panel, status_bar, search_everywhere, @@ -1000,26 +1026,144 @@ impl WorkbenchView { } } + /// 活动窗格 id(`pane_tree.active()` 直读)。 + fn active_pane_id(&self) -> Option { + self.pane_tree.active() + } + + /// 活动窗格编辑器(map 查 active,未命中 `None`)。 + fn active_editor(&self) -> Option> { + self.active_pane_id() + .and_then(|id| self.pane_editors.get(&id).cloned()) + } + + /// 新文件路由编辑器(`route_target()` 落 map,未命中回退 active; + /// 锁定窗格自动跳过由模型保证)。 + fn routed_editor(&self) -> Option> { + if let Some(id) = self.pane_tree.route_target() { + if let Some(ed) = self.pane_editors.get(&id).cloned() { + return Some(ed); + } + } + self.active_editor() + } + + /// 活动编辑器上执行动作(无活动窗格时忽略)。 + fn with_active_editor( + &self, + cx: &mut Context, + f: impl FnOnce(&mut EditorView, &mut Context), + ) { + if let Some(ed) = self.active_editor() { + let _ = ed.update(cx, f); + } + } + + /// 为窗格编辑器装配 pane 归属与拆分/锁定回调(回调经 workbench entity + /// 回写,保证右键菜单 builder 的 `'static` 要求)。 + fn wire_pane_editor( + pane_tree: &PaneTree, + workbench: Entity, + pane_id: PaneId, + editor: &Entity, + cx: &mut Context, + ) { + let wb = workbench.clone(); + let on_split: SplitCallback = Rc::new(move |pid, dir, window, cx: &mut App| { + let _ = wb.update(cx, |this, cx| { + this.split_pane(pid, dir, window, cx); + }); + }); + let wb = workbench; + let on_toggle_lock: ToggleLockCallback = Rc::new(move |pid, cx: &mut App| { + let _ = wb.update(cx, |this, cx| { + this.toggle_pane_lock(pid, cx); + }); + }); + let locked = pane_tree.is_locked(pane_id); + let _ = editor.update(cx, |ed, cx| { + ed.pane_id = Some(pane_id); + ed.pane_locked = locked; + ed.on_split = Some(on_split); + ed.on_toggle_lock = Some(on_toggle_lock); + cx.notify(); + }); + } + + /// 拆分窗格:新建空 `EditorView` + `split_leaf` + 订阅其标签事件; + /// 新窗格留空标签。 + fn split_pane( + &mut self, + pane_id: PaneId, + dir: SplitDir, + window: &mut Window, + cx: &mut Context, + ) { + let Some(new_id) = self.pane_tree.split_leaf(pane_id, dir) else { + return; + }; + let root = self.workspace_root.clone(); + let new_editor = cx.new(|cx| EditorView::new(root, window, cx)); + Self::wire_pane_editor(&self.pane_tree, cx.entity(), new_id, &new_editor, cx); + self.pane_editors.insert(new_id, new_editor.clone()); + let sub = cx.subscribe( + &new_editor, + |this, _ed, event: &EditorTabEvent, cx| match event { + EditorTabEvent::OpenInTerminal { dir } => { + this.send_terminal_command(&format!("cd \"{dir}\""), cx); + } + }, + ); + self._subscriptions.push(sub); + cx.notify(); + } + + /// 关闭窗格:`close_leaf` + 从 map 移除 editor;根叶返回 `false` 时忽略。 + fn close_pane(&mut self, pane_id: PaneId, cx: &mut Context) { + if self.pane_tree.close_leaf(pane_id) { + self.pane_editors.remove(&pane_id); + cx.notify(); + } + } + + /// 翻转窗格锁定并同步编辑器锁定镜像。 + fn toggle_pane_lock(&mut self, pane_id: PaneId, cx: &mut Context) { + self.pane_tree.toggle_lock(pane_id); + if let Some(ed) = self.pane_editors.get(&pane_id).cloned() { + let locked = self.pane_tree.is_locked(pane_id); + let _ = ed.update(cx, |ed, cx| { + ed.pane_locked = locked; + cx.notify(); + }); + } + cx.notify(); + } + /// 持有 `&mut Window` 调用方的编辑器动作直达通道(按键监听等已有 - /// window 的路径使用,避免经窗口句柄二次 `update` 的重入借用失败)。 + /// window 的路径使用,避免经窗口句柄二次 `update` 的重入借用失败); + /// 目标为活动窗格编辑器。 fn editor_direct( &mut self, window: &mut Window, cx: &mut Context, action: impl FnOnce(&mut EditorView, &mut Window, &mut Context), ) { - let _ = self.editor.update(cx, |ed, cx| action(ed, window, cx)); + if let Some(ed) = self.active_editor() { + let _ = ed.update(cx, |ed, cx| action(ed, window, cx)); + } } /// 无 `window` 上下文的编辑器动作经窗口句柄下发(菜单/面板事件路径, /// 非重入场景)。`active_window` 仅反映平台聚焦,Xvfb 下可能为 None, - /// 单窗口应用退化到 `windows()` 首个句柄。 + /// 单窗口应用退化到 `windows()` 首个句柄;目标为活动窗格编辑器。 fn editor_window_action( &self, cx: &mut Context, action: impl FnOnce(&mut EditorView, &mut Window, &mut Context) + 'static, ) { - let editor = self.editor.clone(); + let Some(editor) = self.active_editor() else { + return; + }; let handle = cx .active_window() .or_else(|| cx.windows().into_iter().next()); @@ -1040,8 +1184,11 @@ impl WorkbenchView { /// 目标在工作区内时经 core `file.write`(仿 `save_active`);工作区外 /// core 会拒绝绝对路径,改用 `std::fs` 直写。 fn save_active_as(&mut self, cx: &mut Context) { + let Some(active) = self.active_editor() else { + return; + }; let (old_path, text) = { - let ed = self.editor.read(cx); + let ed = active.read(cx); let Some(idx) = ed.active_tab_index else { return; }; @@ -1069,7 +1216,7 @@ impl WorkbenchView { let root = self.workspace_root.clone(); if let Ok(rel) = dest.strip_prefix(std::path::Path::new(&root)) { let rel = rel.to_string_lossy().to_string(); - let editor = self.editor.clone(); + let editor = active.clone(); let client = self.client.clone(); cx.spawn(async move |_this, cx| { if client.write_file(&cx, &root, &rel, &text).await.is_ok() { @@ -1090,7 +1237,7 @@ impl WorkbenchView { .detach(); } else if std::fs::write(&dest, text.as_bytes()).is_ok() { let abs = dest.to_string_lossy().to_string(); - let _ = self.editor.update(cx, |ed, cx| { + let _ = active.update(cx, |ed, cx| { if let Some(idx) = ed.active_tab_index { if let Some(t) = ed.tabs.get_mut(idx) { if t.path == old_path { @@ -1107,8 +1254,11 @@ impl WorkbenchView { /// 还原文件:重读活动文件内容并灌回编辑器(同路径 `open_file` 会刷新内容并清除脏标记)。 fn revert_active_file(&mut self, cx: &mut Context) { + let Some(active) = self.active_editor() else { + return; + }; let path = { - let ed = self.editor.read(cx); + let ed = active.read(cx); let Some(idx) = ed.active_tab_index else { return; }; @@ -1119,14 +1269,14 @@ impl WorkbenchView { }; if std::path::Path::new(&path).is_absolute() { if let Ok(text) = std::fs::read_to_string(&path) { - let _ = self.editor.update(cx, |ed, cx| { + let _ = active.update(cx, |ed, cx| { ed.open_file(path, text, cx); }); } return; } let root = self.workspace_root.clone(); - let editor = self.editor.clone(); + let editor = active.clone(); let client = self.client.clone(); cx.spawn(async move |_this, cx| { if let Ok(text) = client.read_file(&cx, &root, &path).await { @@ -1159,17 +1309,17 @@ impl WorkbenchView { } match action_id { "workbench.new_file" | "file.new_file" | "file.new_tab" => { - let _ = self.editor.update(cx, |ed, cx| { + self.with_active_editor(cx, |ed, cx| { ed.open_file("untitled.txt".to_string(), String::new(), cx); }); } "workbench.save" | "file.save" => { - let _ = self.editor.update(cx, |ed, cx| { + self.with_active_editor(cx, |ed, cx| { ed.save_active(cx); }); } "workbench.close_tab" | "file.close_editor" => { - let _ = self.editor.update(cx, |ed, cx| { + self.with_active_editor(cx, |ed, cx| { if let Some(idx) = ed.active_tab_index { ed.close_tab(idx, cx); } @@ -1313,7 +1463,7 @@ impl WorkbenchView { self.save_active_as(cx); } "file.save_all" => { - let _ = self.editor.update(cx, |ed, cx| { + self.with_active_editor(cx, |ed, cx| { ed.save_all_tabs(cx); }); } @@ -1321,32 +1471,32 @@ impl WorkbenchView { self.revert_active_file(cx); } "file.close_all" => { - let _ = self.editor.update(cx, |ed, cx| { + self.with_active_editor(cx, |ed, cx| { ed.close_all_tabs(cx); }); } "file.close_others" => { - let _ = self.editor.update(cx, |ed, cx| { + self.with_active_editor(cx, |ed, cx| { ed.close_other_tabs(cx); }); } "file.close_saved" => { - let _ = self.editor.update(cx, |ed, cx| { + self.with_active_editor(cx, |ed, cx| { ed.close_saved_tabs(cx); }); } "file.close_left" => { - let _ = self.editor.update(cx, |ed, cx| { + self.with_active_editor(cx, |ed, cx| { ed.close_tabs_to_left(cx); }); } "file.close_right" => { - let _ = self.editor.update(cx, |ed, cx| { + self.with_active_editor(cx, |ed, cx| { ed.close_tabs_to_right(cx); }); } "file.reopen_closed" => { - let _ = self.editor.update(cx, |ed, cx| { + self.with_active_editor(cx, |ed, cx| { ed.reopen_closed_tab(cx); }); } @@ -1361,7 +1511,7 @@ impl WorkbenchView { self.editor_window_action(cx, |ed, window, cx| ed.cut(window, cx)); } "edit.copy" => { - let _ = self.editor.update(cx, |ed, cx| { + self.with_active_editor(cx, |ed, cx| { ed.copy(cx); }); } @@ -1391,12 +1541,12 @@ impl WorkbenchView { self.open_go_to_line(cx); } "go.next_tab" => { - let _ = self.editor.update(cx, |ed, cx| { + self.with_active_editor(cx, |ed, cx| { ed.goto_next_tab(cx); }); } "go.prev_tab" => { - let _ = self.editor.update(cx, |ed, cx| { + self.with_active_editor(cx, |ed, cx| { ed.goto_prev_tab(cx); }); } @@ -1476,21 +1626,37 @@ impl WorkbenchView { } } - /// 打开指定文件:对接 `lithe-core` 的 `read_file` 并更新编辑器 + /// 打开指定文件:对接 `lithe-core` 的 `read_file` 并更新路由窗格编辑器 + /// (锁定窗格自动跳过由模型保证);成功后将 active 设到路由窗格。 pub fn open_file(&mut self, relative_path: &str, cx: &mut Context) { let root = self.workspace_root.clone(); let path = relative_path.to_string(); - let editor = self.editor.clone(); + let target_pane = self + .pane_tree + .route_target() + .or_else(|| self.pane_tree.active()); + let editor = target_pane + .and_then(|id| self.pane_editors.get(&id).cloned()) + .or_else(|| self.active_editor()); + let Some(editor) = editor else { + return; + }; let client = self.client.clone(); - cx.spawn(async move |_this, cx| { + cx.spawn(async move |this, cx| { let task = client.read_file(&cx, &root, &path); match task.await { Ok(text) => { let _ = editor.update(cx, |ed, cx| { - ed.open_file(path, text, cx); + ed.open_file(path.clone(), text, cx); }); + if let Some(pid) = target_pane { + let _ = this.update(cx, |this, cx| { + this.pane_tree.set_active(pid); + cx.notify(); + }); + } } Err(err) => { let err_msg = format!("// 读取文件失败: {}\n// 错误信息: {}", path, err); @@ -1504,6 +1670,14 @@ impl WorkbenchView { } } +/// 子树首叶(`h/v_resizable` 分组 id 与 ratio 回写定位用)。 +fn pane_first_leaf(node: &PaneNode) -> PaneId { + match node { + PaneNode::Leaf(id) => *id, + PaneNode::Split { first, .. } => pane_first_leaf(first), + } +} + /// 活动栏视图 id → 侧边栏标签。 fn sidebar_tab_for(view_id: &str) -> Option { match view_id { @@ -1532,14 +1706,18 @@ fn this_sync_files(this: &WorkbenchView, cx: &mut Context) { if let Some(root_node) = &this.sidebar.read(cx).root_node { WorkbenchView::collect_all_file_paths(root_node, &mut file_list); } - // 已打开标签页路径同步给快速打开的置顶分组(对齐 Tauri `openBufferFiles`)。 - let open_files: Vec = this - .editor - .read(cx) - .tabs - .iter() - .map(|tab| tab.path.clone()) - .collect(); + // 已打开标签页路径同步给快速打开的置顶分组(对齐 Tauri `openBufferFiles`, + // 多窗格取各窗格 tabs 去重并集)。 + let mut open_files: Vec = Vec::new(); + for leaf in this.pane_tree.leaves() { + if let Some(ed) = this.pane_editors.get(&leaf) { + for tab in ed.read(cx).tabs.iter() { + if !open_files.contains(&tab.path) { + open_files.push(tab.path.clone()); + } + } + } + } if !file_list.is_empty() { let _ = this.search_everywhere.update(cx, |search, cx| { search.set_files(file_list.clone(), cx); @@ -1559,11 +1737,13 @@ impl Render for WorkbenchView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let show_status_bar = settings::get(cx).show_status_bar; - // 通知诊断行点击的待跳转:文件加载完成后跳到指定行(1 起)。 + // 通知诊断行点击的待跳转:文件加载完成后跳到指定行(1 起,活动窗格)。 if let Some(line) = self.pending_goto_line.take() { - let _ = self.editor.update(cx, |ed, cx| { - ed.go_to_line(line, window, cx); - }); + if let Some(active) = self.active_editor() { + let _ = active.update(cx, |ed, cx| { + ed.go_to_line(line, window, cx); + }); + } } // 右侧铃铛角标与通知未读数同步(变化时才 notify,避免渲染循环)。 { @@ -1757,7 +1937,7 @@ impl WorkbenchView { .w_full() .min_h_0() .bg(ThemeColors::background()) - .child(self.editor.clone()), + .child(self.render_editor_area(cx)), ) .when(bottom_visible, |layout| layout.child(bottom_splitter)) .when(bottom_visible, |layout| { @@ -1811,6 +1991,100 @@ impl WorkbenchView { }) } + /// 编辑区:窗格树递归渲染(空树回退空占位)。 + fn render_editor_area(&self, cx: &mut Context) -> AnyElement { + match self.pane_tree.root().cloned() { + Some(root) => self.render_pane_node(&root, cx), + None => div().size_full().into_any_element(), + } + } + + /// 递归渲染窗格节点:`Split{Horizontal}` 左右并排(`h_resizable`), + /// `{Vertical}` 上下堆叠(`v_resizable`);拖拽比例经 `set_ratio` 回写。 + fn render_pane_node(&self, node: &PaneNode, cx: &mut Context) -> AnyElement { + match node { + PaneNode::Leaf(id) => self.render_pane_leaf(*id, cx), + PaneNode::Split { + dir, first, second, .. + } => { + let first_el = self.render_pane_node(first, cx); + let second_el = self.render_pane_node(second, cx); + let group_id = format!( + "pane-split-{}-{}", + pane_first_leaf(first), + pane_first_leaf(second) + ); + let ratio_leaf = pane_first_leaf(first); + let view = cx.entity(); + let on_resize = move |state: &Entity, _: &mut Window, cx: &mut App| { + let sizes = state.read(cx).sizes().clone(); + if sizes.len() < 2 { + return; + } + let total = sizes[0].as_f32() + sizes[1].as_f32(); + if total <= 0.0 { + return; + } + let ratio = sizes[0].as_f32() / total; + let _ = view.update(cx, |this, cx| { + this.pane_tree.set_ratio(ratio_leaf, ratio); + cx.notify(); + }); + }; + if *dir == SplitDir::Horizontal { + h_resizable(group_id) + .on_resize(on_resize) + .child(resizable_panel().child(first_el)) + .child(resizable_panel().child(second_el)) + .into_any_element() + } else { + v_resizable(group_id) + .on_resize(on_resize) + .child(resizable_panel().child(first_el)) + .child(resizable_panel().child(second_el)) + .into_any_element() + } + } + } + } + + /// 渲染单个窗格叶:对应 editor;整个叶包左键设 active;空标签叶居中 + /// 显示关闭窗格按钮(根叶关闭返回 `false` 时忽略)。 + fn render_pane_leaf(&self, id: PaneId, cx: &mut Context) -> AnyElement { + let Some(editor) = self.pane_editors.get(&id).cloned() else { + return div().size_full().into_any_element(); + }; + let is_empty = editor.read(cx).tabs.is_empty(); + let base = v_flex() + .size_full() + .min_w_0() + .min_h_0() + .bg(ThemeColors::background()) + .on_mouse_down( + MouseButton::Left, + cx.listener(move |this, _event: &MouseDownEvent, _window, cx| { + this.pane_tree.set_active(id); + cx.notify(); + }), + ); + if is_empty { + base.items_center() + .justify_center() + .child( + Button::new(format!("close-pane-{id}")) + .small() + .ghost() + .label(crate::i18n::menu_text(cx, "ui.close").to_string()) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.close_pane(id, cx); + })), + ) + .into_any_element() + } else { + base.child(editor).into_any_element() + } + } + fn render_sidebar_splitter(&self, cx: &mut Context) -> impl IntoElement { div() .id("sidebar-splitter") From ed49c7ba25282227f7fdad31c1f01b185edf9f41 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 00:44:52 +0800 Subject: [PATCH 37/59] =?UTF-8?q?fix(linux):=20=E6=8F=92=E4=BB=B6=E9=A1=B5?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=20Tauri=20=E6=89=A9=E5=B1=95=E4=BE=A7?= =?UTF-8?q?=E6=A0=8F=E8=AF=AD=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 9 分类签带计数(搜索命中内计数),替代 5 项下拉 - 行点选代替行内开关;未启用行内加号一键启用 - 列表下加详情区:徽标/发布者/版本/启用停用/贡献项 - 文案键与 locale.ts 同键;删无用 has_run_history --- linux/src/i18n.rs | 21 + linux/src/workbench/bottom_panel.rs | 5 - linux/src/workbench/extensions_panel.rs | 637 ++++++++++++++++++------ 3 files changed, 493 insertions(+), 170 deletions(-) diff --git a/linux/src/i18n.rs b/linux/src/i18n.rs index b4a62c637..997877c87 100644 --- a/linux/src/i18n.rs +++ b/linux/src/i18n.rs @@ -180,6 +180,27 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { // ---- 空态 / 面板标题 ---- "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"), // ---- Git ---- "git.commit" => ("提交", "Commit"), diff --git a/linux/src/workbench/bottom_panel.rs b/linux/src/workbench/bottom_panel.rs index 6838914f5..237b567f1 100644 --- a/linux/src/workbench/bottom_panel.rs +++ b/linux/src/workbench/bottom_panel.rs @@ -199,11 +199,6 @@ impl BottomPanelView { cx.notify(); } - /// 是否有运行历史;宿主据此决定左侧栏是否展示 maven 项。 - pub fn has_run_history(&self) -> bool { - !self.run_history.is_empty() - } - /// 更新工作目录并重探 Run 工程(替代直接写 `working_dir` 字段)。 pub fn set_working_dir(&mut self, dir: String, cx: &mut Context) { self.working_dir = dir; diff --git a/linux/src/workbench/extensions_panel.rs b/linux/src/workbench/extensions_panel.rs index 8072a6c71..8e6cf0065 100644 --- a/linux/src/workbench/extensions_panel.rs +++ b/linux/src/workbench/extensions_panel.rs @@ -1,11 +1,13 @@ -//! 右侧扩展面板:搜索 + 分类 + 列表 + 启用开关,对齐 Tauri +//! 右侧扩展面板:搜索 + 分类签 + 列表点选 + 详情,对齐 Tauri //! `windows/tauri/src/extensions/ui/components/extensions-sidebar.tsx` -//! 的搜索与分类交互语义。 +//! 的展示语义(标题计数、分类签计数、行点选、详情徽标与主操作)。 //! -//! 平台适配说明:Tauri 的扩展在中央开 buffer 展示详情,Linux 中央是编辑器 -//! Tab,因此扩展列表改走右侧面板;交互保持一致(搜索框过滤、分类下拉、 -//! 列表行、启用开关)。头部 X 只发射 [`ExtensionsEvent::Close`],由外部 -//! (`view.rs`)决定面板显隐,与 [`crate::workbench::maven::MavenView`] 一致。 +//! 平台适配说明: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}; @@ -13,38 +15,44 @@ 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::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 _, - Subscription, Window, + 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 `extensionsActiveTab` 的 Linux 子集(`all` 为过滤器, -/// 其余为归类目标;`icon-theme`/`database`/`skill`/`agent` 暂归 `integration`)。 +/// 扩展分类:与 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; 5] = [ + /// 分类签的固定顺序,与 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 对齐。 @@ -53,19 +61,27 @@ impl ExtensionCategory { 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", } } - /// 下拉展示文案:`i18n.rs` 无对应键,按约定英文直写。 - pub fn label(self) -> &'static str { + /// 分类签图标:对齐 Tauri 各 tab 图标;`all` 无图标。 + pub fn icon(self) -> Option { match self { - ExtensionCategory::All => "All", - ExtensionCategory::Language => "Languages", - ExtensionCategory::Theme => "Themes", - ExtensionCategory::Ai => "AI", - ExtensionCategory::Integration => "Integrations", + 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), } } @@ -74,14 +90,18 @@ impl ExtensionCategory { "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。 @@ -90,6 +110,10 @@ pub struct ExtensionItem { pub version: String, pub description: String, pub category: ExtensionCategory, + /// 发布者:`package.json` 的 publisher/author,缺失为 `None`(不展示)。 + pub publisher: Option, + /// 是否随产品发布(对齐 Tauri `isBundled` 徽标)。 + pub is_bundled: bool, } /// 扩展面板派发的事件。 @@ -98,7 +122,7 @@ pub enum ExtensionsEvent { Close, } -/// 右侧扩展面板:搜索框 + 分类下拉 + 列表 + 启用开关。 +/// 右侧扩展面板:搜索框 + 分类签 + 列表点选 + 详情。 pub struct ExtensionsView { items: Vec, /// 启用状态 `{id: bool}`,缺省视为启用,落盘到 @@ -107,6 +131,9 @@ pub struct ExtensionsView { search_input: Option>, _search_subscription: Option, category: ExtensionCategory, + /// 选中的扩展 id(对齐 Tauri `selectedExtensionId`;过滤后不在列表 + /// 则展示回退到首项,不在此处改写)。 + selected: Option, } impl EventEmitter for ExtensionsView {} @@ -119,12 +146,13 @@ impl ExtensionsView { 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()); @@ -132,6 +160,11 @@ impl ExtensionsView { 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; } @@ -167,14 +200,11 @@ impl ExtensionsView { .unwrap_or_default() } - /// 搜索 + 分类双重过滤,对齐 Tauri 的 `filteredExtensions`。 - fn filtered_items(&self, query: &str) -> Vec<&ExtensionItem> { + /// 搜索过滤(先搜索后分签,对齐 Tauri `searchMatchedExtensions`)。 + fn search_matched(&self, query: &str) -> Vec<&ExtensionItem> { let q = query.trim().to_lowercase(); self.items .iter() - .filter(|item| { - self.category == ExtensionCategory::All || item.category == self.category - }) .filter(|item| { q.is_empty() || item.name.to_lowercase().contains(&q) @@ -184,99 +214,262 @@ impl ExtensionsView { .collect() } - /// 分类下拉:当前值按钮 + ChevronDown(复用 `settings_dialog.rs` - /// `render_dropdown` 思路:选中项打勾)。 - fn render_category_dropdown(&self, cx: &mut Context) -> impl IntoElement { - let view = cx.entity(); - let current = self.category.as_str(); - let current_label = self.category.label().to_string(); - let options: Vec<(&'static str, String)> = ExtensionCategory::ALL - .iter() - .map(|c| (c.as_str(), c.label().to_string())) - .collect(); - Button::new("extensions-category") - .small() - .ghost() - .rounded_md() - .border_1() - .border_color(ThemeColors::border()) - .bg(ThemeColors::background()) - .w(px(160.0)) - .child( - div() - .flex_1() - .text_xs() - .text_color(ThemeColors::foreground()) - .child(current_label), - ) - .child( - Icon::new(IconName::ChevronDown) - .size(px(13.0)) - .text_color(ThemeColors::subtle_foreground()), - ) - .dropdown_menu(move |menu, _window, _cx| { - let mut menu = menu; - for (value, label) in &options { - let v = view.clone(); - let value = *value; - let label = label.clone(); - let selected = value == current; - 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(category) = ExtensionCategory::from_str(value) { - this.category = category; - cx.notify(); - } - }); - })); - } - menu - }) + /// 各分类签计数(搜索命中内部分类计数,对齐 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 } - /// 启用开关:可点击药丸,圆点指示状态(对齐 `settings_dialog.rs` - /// `render_toggle`,无文字)。 - fn render_toggle(&self, id: &str, value: bool, cx: &mut Context) -> impl IntoElement { - let id_owned = id.to_string(); + /// 分类签行:横滑签 + 计数角标,对齐 Tauri `FILTER_TABS` 行。 + fn render_tab_row(&self, counts: &[usize; 9], cx: &mut Context) -> impl IntoElement { h_flex() - .id(format!("ext-toggle-{id_owned}")) - .items_center() - .gap_1p5() - .px_2p5() + .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() - .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()) - }) + .border_color(if highlighted { + ThemeColors::accent_blue() + } else { + ThemeColors::border() }) - .child(div().w(px(8.0)).h(px(8.0)).rounded_full().bg(if value { - ThemeColors::foreground() + .text_color(if highlighted { + ThemeColors::accent_blue() } else { - ThemeColors::subtle_foreground() - })) - .on_click(cx.listener(move |this, _event, _window, cx| { - this.toggle(&id_owned, cx); - })) + 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 { @@ -294,70 +487,130 @@ impl Render for ExtensionsView { )); } let query = self.search_query(cx); - let filtered = self.filtered_items(&query); - let enabled_snapshot = self.enabled.clone(); + 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 = enabled_snapshot.get(&item.id).copied().unwrap_or(true); - let name = item.name.clone(); - let version = item.version.clone(); - let description = item.description.clone(); + 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_2() - .px_2p5() + .gap_2p5() + .px_2() .py_1p5() - .rounded_sm() - .hover(|h| h.bg(ThemeColors::bg_tab_hover())) + .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( - Icon::new(IconName::Puzzle) - .size(px(14.0)) - .text_color(ThemeColors::subtle_foreground()), + 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() - .gap_0p5() - .overflow_hidden() + .min_w_0() .child( - h_flex() - .items_center() - .gap_1p5() - .child( - div() - .text_xs() - .truncate() - .text_color(if enabled { - ThemeColors::text_primary() - } else { - ThemeColors::text_muted() - }) - .child(name), - ) - .child( - div() - .text_xs() - .flex_shrink_0() - .text_color(ThemeColors::text_muted()) - .child(version), - ), + 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(description), + .child(item.description.clone()), ), ) - .child(self.render_toggle(&item.id, enabled, cx).into_any_element()) + .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() @@ -375,11 +628,23 @@ impl Render for ExtensionsView { .justify_between() .px_3() .child( - div() - .text_xs() - .font_weight(FontWeight::BOLD) - .text_color(ThemeColors::text_muted()) - .child(crate::i18n::menu_text(cx, "extensions.title")), + 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") @@ -395,16 +660,16 @@ impl Render for ExtensionsView { .child( v_flex() .w_full() - .gap_2() - .p_2() - .border_b_1() - .border_color(ThemeColors::border()) + .flex_shrink_0() + .gap_1() + .px_2() + .pt_2() .child( div() .w_full() .child(Input::new(&search_entity).cleanable(true)), ) - .child(self.render_category_dropdown(cx)), + .child(self.render_tab_row(&counts, cx)), ) .child(if rows.is_empty() { div() @@ -415,16 +680,31 @@ impl Render for ExtensionsView { .justify_center() .text_xs() .text_color(ThemeColors::text_muted()) - .child("No extensions found") + .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), + ) + }) } } @@ -437,6 +717,8 @@ fn builtin_items() -> Vec { 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(), @@ -444,6 +726,8 @@ fn builtin_items() -> Vec { 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(), @@ -451,6 +735,8 @@ fn builtin_items() -> Vec { version: "0.1.0".to_string(), description: "Integrated terminal".to_string(), category: ExtensionCategory::Integration, + publisher: None, + is_bundled: true, }, ] } @@ -528,6 +814,8 @@ struct PackageJson { version: Option, description: Option, keywords: Option>, + publisher: Option, + author: Option, } #[derive(Debug, serde::Deserialize)] @@ -549,6 +837,10 @@ fn read_manifest(dir: &Path, dir_name: &str) -> Option { 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 @@ -558,6 +850,8 @@ fn read_manifest(dir: &Path, dir_name: &str) -> Option { 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"); @@ -581,6 +875,8 @@ fn read_manifest(dir: &Path, dir_name: &str) -> Option { version: manifest.version.unwrap_or_default(), description: manifest.description.unwrap_or_default(), category, + publisher: None, + is_bundled: false, }); } None @@ -591,16 +887,27 @@ fn classify_keywords(keywords: &[String]) -> ExtensionCategory { classify_text(&keywords.join(" ")) } -/// 关键词归类:theme → 主题,language/lang/lsp/syntax → 语言, -/// ai/llm/agent/chat/model → AI,其余 → integration。 +/// 关键词归类: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(&["theme", "icon", "color-scheme"]) { + 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(&["ai", "llm", "agent", "chat", "model"]) { + } 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 From 95cbf6041255d13d33bfaa9b63189af2ad9f7175 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 00:46:42 +0800 Subject: [PATCH 38/59] =?UTF-8?q?fix(linux):=20=E7=82=B9=E6=89=A9=E5=B1=95?= =?UTF-8?q?=E6=97=B6=E5=8F=B3=E4=BE=A7=E9=BB=98=E8=AE=A4=E5=B1=95=E5=BC=80?= =?UTF-8?q?=E5=88=B0=20560?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- linux/src/workbench/view.rs | 40 +++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/linux/src/workbench/view.rs b/linux/src/workbench/view.rs index 570e61f77..f616a4a63 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -905,14 +905,19 @@ impl WorkbenchView { cx.notify(); } - /// 切换到指定项目根路径:同步侧边栏、Maven 与欢迎页状态,并记录最近项目。 /// 右侧工具窗口 toggle(对齐 `resolveRightToolWindowUpdate`): - /// 同视图再点关闭,否则切换视图并展开。 + /// 同视图再点关闭,否则切换视图并展开。扩展页内容多(列表+详情), + /// 打开时宽度不足则默认撑到 560(用户拖过的更宽值保留)。 fn toggle_right_tool(&mut self, view: RightToolView, cx: &mut Context) { if self.right_tool == Some(view) { self.right_tool = None; } else { self.right_tool = Some(view); + if view == RightToolView::Extensions + && settings::get(cx).right_tool_window_width < 560.0 + { + settings::update(cx, |s| s.right_tool_window_width = 560.0); + } } cx.notify(); } @@ -2016,21 +2021,22 @@ impl WorkbenchView { ); let ratio_leaf = pane_first_leaf(first); let view = cx.entity(); - let on_resize = move |state: &Entity, _: &mut Window, cx: &mut App| { - let sizes = state.read(cx).sizes().clone(); - if sizes.len() < 2 { - return; - } - let total = sizes[0].as_f32() + sizes[1].as_f32(); - if total <= 0.0 { - return; - } - let ratio = sizes[0].as_f32() / total; - let _ = view.update(cx, |this, cx| { - this.pane_tree.set_ratio(ratio_leaf, ratio); - cx.notify(); - }); - }; + let on_resize = + move |state: &Entity, _: &mut Window, cx: &mut App| { + let sizes = state.read(cx).sizes().clone(); + if sizes.len() < 2 { + return; + } + let total = sizes[0].as_f32() + sizes[1].as_f32(); + if total <= 0.0 { + return; + } + let ratio = sizes[0].as_f32() / total; + let _ = view.update(cx, |this, cx| { + this.pane_tree.set_ratio(ratio_leaf, ratio); + cx.notify(); + }); + }; if *dir == SplitDir::Horizontal { h_resizable(group_id) .on_resize(on_resize) From 4910f1195801bdb9ba5204e0ae11151e65c649c6 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 00:50:19 +0800 Subject: [PATCH 39/59] =?UTF-8?q?fix(linux):=20=E9=80=9A=E7=9F=A5=E7=82=B9?= =?UTF-8?q?=E5=87=BB=E4=B8=8E=E6=96=87=E6=A1=88=E5=AF=B9=E9=BD=90=20Tauri?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 点击行标已读+展开详情(含删除),诊断行保留跳文件 - 类型筛选签行;头部加全部清除;空构造对齐空态 - 打开通知页即全标已读;全部文案走 notifications.* 键 --- linux/src/i18n.rs | 12 ++ linux/src/workbench/notifications.rs | 204 ++++++++++++++++++++++----- linux/src/workbench/view.rs | 9 +- 3 files changed, 192 insertions(+), 33 deletions(-) diff --git a/linux/src/i18n.rs b/linux/src/i18n.rs index 997877c87..5a8506675 100644 --- a/linux/src/i18n.rs +++ b/linux/src/i18n.rs @@ -202,6 +202,18 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { "extensions.activate" => ("启用", "Activate"), "extensions.deactivate" => ("停用", "Deactivate"), "notifications.title" => ("通知", "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.* 逐字对齐) ---- diff --git a/linux/src/workbench/notifications.rs b/linux/src/workbench/notifications.rs index bae2fe58e..7a3f9ec8d 100644 --- a/linux/src/workbench/notifications.rs +++ b/linux/src/workbench/notifications.rs @@ -29,7 +29,7 @@ pub enum NotificationsEvent { } /// 通知类型,对齐 Tauri `NotificationEntry["type"]`;`Diagnostic` 为 Linux 侧 -/// 由诊断快照投递的定位类通知,点击时额外发出 `OpenFile`。 +/// 由诊断快照投递的定位类通知,点击时额外发出 `OpenFile`,筛选时归入错误桶。 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NotificationKind { Info, @@ -41,6 +41,50 @@ pub enum NotificationKind { 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", + } + } +} + /// 单条通知:诊断类通过 `path` 携带定位信息,非诊断类为 `None`。 #[derive(Debug, Clone)] pub struct NotificationItem { @@ -58,38 +102,22 @@ pub struct NotificationsView { items: Vec, next_id: u64, search: Option>, + filter: NotificationFilter, + /// 展开详情的通知 id(对齐 Tauri 点击行打开详情)。 + selected: Option, } impl EventEmitter for NotificationsView {} impl NotificationsView { - /// 构造并预置 2 条未读提示,保证面板首次打开非空。 + /// 构造空列表(对齐 Tauri:无通知时展示空态,不预置文案)。 pub fn new(_cx: &mut Context) -> Self { Self { - items: vec![ - NotificationItem { - id: 0, - title: "Welcome to Lithe".to_string(), - body: "Notifications from the workspace and language services appear here." - .to_string(), - kind: NotificationKind::Info, - read: false, - path: None, - line: 1, - }, - NotificationItem { - id: 1, - title: "Tip: diagnostics are clickable".to_string(), - body: "Click a diagnostic notification to jump to its file location." - .to_string(), - kind: NotificationKind::Info, - read: false, - path: None, - line: 1, - }, - ], - next_id: 2, + items: Vec::new(), + next_id: 0, search: None, + filter: NotificationFilter::All, + selected: None, } } @@ -150,7 +178,7 @@ impl NotificationsView { .count() } - /// 全部标为已读。 + /// 全部标为已读(宿主在打开面板时调用,对齐 Tauri 打开即已读)。 pub fn mark_all_read(&mut self, cx: &mut Context) { for item in &mut self.items { item.read = true; @@ -158,9 +186,28 @@ impl NotificationsView { cx.notify(); } - fn toggle_read(&mut self, id: u64, cx: &mut Context) { + /// 删除一条通知;删的是展开项则收起详情。 + 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 = !item.read; + item.read = true; + self.selected = Some(id); } cx.notify(); } @@ -201,9 +248,12 @@ impl Render for NotificationsView { let search_entity = self.ensure_search(window, cx); let query = self.query(cx); let unread = self.unread_count(); + let filter = self.filter; + 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) @@ -219,6 +269,7 @@ impl Render for NotificationsView { 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}")) @@ -230,6 +281,7 @@ impl Render for NotificationsView { .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() @@ -271,8 +323,41 @@ impl Render for NotificationsView { .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.toggle_read(id, cx); + this.open_details(id, cx); if is_diagnostic { if let Some(path) = path.clone() { cx.emit(NotificationsEvent::OpenFile(path, line)); @@ -327,12 +412,30 @@ impl Render for NotificationsView { Button::new("notifications-mark-read") .small() .ghost() - .label("Mark all read") + .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() @@ -363,6 +466,43 @@ impl Render for NotificationsView { .child(Input::new(&search_entity).cleanable(true)), ), ) + .child( + h_flex() + .w_full() + .flex_shrink_0() + .gap_1() + .px_2() + .pb_1() + .overflow_x_scrollbar() + .children(NotificationFilter::ALL.iter().map(|kind| { + let active = filter == *kind; + let id = *kind; + h_flex() + .id(format!("notification-filter-{}", id.label_key())) + .flex_shrink_0() + .items_center() + .h(px(24.0)) + .px_2() + .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()) + }) + }) + .child(crate::i18n::menu_text(cx, id.label_key())) + .on_click(cx.listener(move |this, _event, _window, cx| { + this.filter = id; + cx.notify(); + })) + })), + ) .child(if is_empty { div() .flex_1() @@ -372,7 +512,7 @@ impl Render for NotificationsView { .justify_center() .text_xs() .text_color(ThemeColors::text_muted()) - .child("No notifications") + .child(crate::i18n::menu_text(cx, "notifications.empty")) .into_any_element() } else if items.is_empty() { div() @@ -383,7 +523,7 @@ impl Render for NotificationsView { .justify_center() .text_xs() .text_color(ThemeColors::text_muted()) - .child("No matching notifications") + .child(crate::i18n::menu_text(cx, "notifications.noMatch")) .into_any_element() } else { div() diff --git a/linux/src/workbench/view.rs b/linux/src/workbench/view.rs index f616a4a63..f269da00d 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -908,6 +908,7 @@ impl WorkbenchView { /// 右侧工具窗口 toggle(对齐 `resolveRightToolWindowUpdate`): /// 同视图再点关闭,否则切换视图并展开。扩展页内容多(列表+详情), /// 打开时宽度不足则默认撑到 560(用户拖过的更宽值保留)。 + /// 通知页打开即全标已读(对齐 Tauri 打开工具窗即已读)。 fn toggle_right_tool(&mut self, view: RightToolView, cx: &mut Context) { if self.right_tool == Some(view) { self.right_tool = None; @@ -918,6 +919,11 @@ impl WorkbenchView { { settings::update(cx, |s| s.right_tool_window_width = 560.0); } + if view == RightToolView::Notifications { + let _ = self.notifications.update(cx, |n, cx| { + n.mark_all_read(cx); + }); + } } cx.notify(); } @@ -970,8 +976,9 @@ impl WorkbenchView { bp.set_working_dir(path.clone(), cx); }); // 右侧通知中心投递项目打开事件(对齐 Tauri 系统事件通知)。 + let opened = crate::i18n::menu_text(cx, "notifications.projectOpened").to_string(); let _ = self.notifications.update(cx, |n, cx| { - n.push("已打开项目", path.clone(), cx); + n.push(opened, path.clone(), cx); }); cx.notify(); } From 03df5d47330236f4e82e2d4c050764ad8c0e1e3d Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 00:55:16 +0800 Subject: [PATCH 40/59] =?UTF-8?q?fix(linux):=20=E9=80=9A=E7=9F=A5=E7=AD=9B?= =?UTF-8?q?=E9=80=89=E6=94=B9=E6=BC=8F=E6=96=97=E4=B8=8B=E6=8B=89=EF=BC=8C?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=20Tauri=20=E7=89=88=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- linux/src/i18n.rs | 1 + linux/src/workbench/notifications.rs | 101 +++++++++++++++++---------- 2 files changed, 65 insertions(+), 37 deletions(-) diff --git a/linux/src/i18n.rs b/linux/src/i18n.rs index 5a8506675..970e760fe 100644 --- a/linux/src/i18n.rs +++ b/linux/src/i18n.rs @@ -202,6 +202,7 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { "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"), diff --git a/linux/src/workbench/notifications.rs b/linux/src/workbench/notifications.rs index 7a3f9ec8d..6f9d41047 100644 --- a/linux/src/workbench/notifications.rs +++ b/linux/src/workbench/notifications.rs @@ -10,6 +10,7 @@ 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 _; @@ -83,6 +84,27 @@ impl NotificationFilter { 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`。 @@ -249,6 +271,16 @@ impl Render for NotificationsView { 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 @@ -464,45 +496,40 @@ impl Render for NotificationsView { 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( - h_flex() - .w_full() - .flex_shrink_0() - .gap_1() - .px_2() - .pb_1() - .overflow_x_scrollbar() - .children(NotificationFilter::ALL.iter().map(|kind| { - let active = filter == *kind; - let id = *kind; - h_flex() - .id(format!("notification-filter-{}", id.label_key())) - .flex_shrink_0() - .items_center() - .h(px(24.0)) - .px_2() - .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()) - }) - }) - .child(crate::i18n::menu_text(cx, id.label_key())) - .on_click(cx.listener(move |this, _event, _window, cx| { - this.filter = id; - cx.notify(); - })) - })), - ) .child(if is_empty { div() .flex_1() From 2a494e515ebe1f1ddef180b57c219bf4c2987ad5 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 01:08:13 +0800 Subject: [PATCH 41/59] =?UTF-8?q?fix(linux):=20Maven=20=E5=B1=95=E5=BC=80?= =?UTF-8?q?=E9=9D=A2=E6=9D=BF=E5=AF=B9=E9=BD=90=20Tauri=20=E6=A0=91?= =?UTF-8?q?=E8=AF=AD=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 模块树(artifactId+packaging 副标题,嵌套 modules,引导线) - caret 与标签分离;phase 单击选中、双击运行 - 依赖嵌套树 + Tauri 同式副标题 + omitted 标记 + 重试/取消 - 工具栏:运行选中/目标文本/reload/跳过测试/全部折叠/设置 - Profiles 勾选与 skipTests 进 launchPlan;任务标题 phase·artifactId - 失败重试面板;全部文案走 maven.* 键 --- linux/src/i18n.rs | 20 + linux/src/workbench/bottom_panel.rs | 40 +- linux/src/workbench/maven.rs | 1701 ++++++++++++++++++++------- linux/src/workbench/view.rs | 19 +- 4 files changed, 1346 insertions(+), 434 deletions(-) diff --git a/linux/src/i18n.rs b/linux/src/i18n.rs index 970e760fe..3dcd560a8 100644 --- a/linux/src/i18n.rs +++ b/linux/src/i18n.rs @@ -173,6 +173,26 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { "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"), diff --git a/linux/src/workbench/bottom_panel.rs b/linux/src/workbench/bottom_panel.rs index 237b567f1..1d2aec2f4 100644 --- a/linux/src/workbench/bottom_panel.rs +++ b/linux/src/workbench/bottom_panel.rs @@ -385,10 +385,18 @@ impl BottomPanelView { } /// 运行 Maven 目标:切 Maven 页 + 经 core `maven.launchPlan` 拿确定性参数 - ///(payload `{root, context: {version: 1, reactorPath: "."}, module, goals}`), + ///(payload `{root, context: {version: 1, reactorPath: ".", profiles, skipTests}, module, goals}`), /// 起受管进程并把输出泵入 Maven 页(首行 `$ mvn …`,对齐 Tauri Maven 页)。 /// 已有任务在跑时先停掉(对齐 Tauri 停掉上一个 session)。 - pub fn run_maven_goal(&mut self, pom_path: &str, goal: &str, cx: &mut Context) { + 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; @@ -402,10 +410,7 @@ impl BottomPanelView { } self.maven_seq += 1; let seq = self.maven_seq; - let title = format!( - "{goal} · {}", - maven_display_pom(&self.working_dir, pom_path) - ); + let title = format!("{goal} · {target}"); self.active_tab = BottomTab::Maven; self.is_collapsed = false; self.maven_title = Some(title.clone()); @@ -416,6 +421,7 @@ impl BottomPanelView { 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 child_slot = self.maven_child.clone(); cx.spawn(async move |this, cx| { let plan = client @@ -424,7 +430,12 @@ impl BottomPanelView { "maven.launchPlan", serde_json::json!({ "root": root, - "context": { "version": 1, "reactorPath": "." }, + "context": { + "version": 1, + "reactorPath": ".", + "profiles": profiles, + "skipTests": skip_tests, + }, "module": module, "goals": [goal], }), @@ -1468,21 +1479,6 @@ fn plan_to_steps( fallback_run_steps(item, root) } -/// Maven 页展示用的 pom 路径:相对工作区,根 pom 显示 `pom.xml`。 -fn maven_display_pom(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()) -} - /// pom 所在目录相对 root 的模块路径:根 pom 为 `.`,与 core 模块约定一致。 fn maven_module_for_pom(root: &str, pom: &str) -> String { let relative = std::path::Path::new(pom) diff --git a/linux/src/workbench/maven.rs b/linux/src/workbench/maven.rs index ce6ea2333..84673fc3f 100644 --- a/linux/src/workbench/maven.rs +++ b/linux/src/workbench/maven.rs @@ -8,7 +8,7 @@ //! `cx.spawn` 里异步调用 core 命令;本地 pom 发现([`find_pom_files`])先行, //! core 失败只落工程/依赖状态,不阻塞导航展示。 -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use gpui_kit::assets::IconName; use gpui_kit::component::button::{Button, ButtonVariants as _}; @@ -17,9 +17,9 @@ 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 _, Context, Entity, EventEmitter, FontWeight, InteractiveElement as _, - IntoElement, ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, - Subscription, Window, + 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; @@ -84,25 +84,43 @@ fn walk_pom_dir(dir: &std::path::Path, depth: usize, out: &mut Vec) { } /// Lifecycle 组的展开键。 -fn lifecycle_key(pom: &str) -> String { - format!("{pom}#lifecycle") +fn lifecycle_key(owner: &str) -> String { + format!("{owner}:lifecycle") } /// 依赖组的展开键。 -fn deps_key(pom: &str) -> String { - format!("{pom}#deps") +fn deps_key(owner: &str) -> String { + format!("{owner}:dependencies") } -/// phase 行的稳定节点 id,同时用作选中键。 -fn phase_id(pom: &str, phase: &str) -> String { - format!("{pom}#lifecycle#{phase}") +/// 源码根目录组的展开键。 +fn source_roots_key(owner: &str) -> String { + format!("{owner}:source-roots") } -/// 从选中行 id 反推所属 pom:只有 phase 行可反推,其余返回空。 -fn selected_pom(selected: &Option) -> Option { - selected - .as_ref() - .and_then(|id| id.split_once("#lifecycle#").map(|(pom, _)| pom.to_string())) +/// 模块节点的展开键与 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`。 @@ -133,61 +151,189 @@ fn relative_pom_paths(root: &str, poms: &[String]) -> Vec { .collect() } -/// pom 所在目录相对 root 的模块路径:根 pom 为 `.`,与 core 的模块路径约定一致。 -fn module_path_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 `projectStatus` 的工程状态:`idle` / `loading` / `ready` / `failed`。 +/// 对齐 Tauri `projectStatus` 的工程状态:`idle` / `loading` / `ready` / `failed`; +/// 依赖加载另有 `Cancelled`(用户主动取消,可重试)。 #[derive(Debug, Clone, PartialEq, Eq)] pub enum MavenStatus { Idle, Loading, Ready, Failed(String), + Cancelled, } -/// 依赖列表行的最小展示字段,来自 `maven.dependencies` 返回的 -/// `dependencies[]`(`groupId` / `artifactId` / `version` / `scope`)。 +/// 依赖列表行的完整字段,对齐 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, + }) } -/// 从 `maven.dependencies` 的返回体解析依赖列表,缺失字段的行直接丢弃。 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(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(), - scope: item - .get("scope") - .and_then(|value| value.as_str()) - .unwrap_or_default() - .to_string(), + 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() @@ -195,31 +341,49 @@ fn parse_deps(data: &serde_json::Value) -> Vec { .unwrap_or_default() } -/// Maven 导航面板派发的事件:执行只发射,由外部经底部 Terminal 执行。 +/// Maven 导航面板派发的事件:执行只发射,由外部经底部 Maven 页执行; +/// 依赖行点击发射 `OpenFile`(打开模块 pom),设置按钮发射 `OpenSettings`。 #[derive(Debug, Clone)] pub enum MavenEvent { - RunGoal { pom_path: String, phase: String }, + RunGoal { + pom_path: String, + phase: String, + /// 展示用目标(`{phase} · {artifactId}`,对齐 Tauri 任务标题)。 + target: String, + profiles: Vec, + skip_tests: bool, + }, + OpenFile(String), + OpenSettings, Close, } -/// 右侧 Maven 导航工具窗口:项目树 + 生命周期阶段 + 依赖 + 自定义 goal。 +/// 右侧 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, - selected: Option, + /// 选中的模块相对路径;`None` 为根项目。 + selected_module: Option, + selected_phase: String, project_status: MavenStatus, - project_name: Option, - dependencies: Vec, - dep_status: MavenStatus, - /// 一次只展开一个模块的依赖组,存 pom 绝对路径。 - expanded_deps: Option, + 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, - dep_seq: u64, } impl EventEmitter for MavenView {} @@ -229,19 +393,23 @@ impl MavenView { let mut view = Self { root, poms: Vec::new(), + project: None, + profiles: Vec::new(), + selected_profiles: HashSet::new(), + skip_tests: false, expanded: HashSet::new(), - selected: None, + selected_module: None, + selected_phase: "compile".to_string(), project_status: MavenStatus::Idle, - project_name: None, - dependencies: Vec::new(), - dep_status: MavenStatus::Idle, - expanded_deps: None, + 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, - dep_seq: 0, }; view.reload(cx); view @@ -249,24 +417,35 @@ impl MavenView { pub fn set_root(&mut self, root: String, cx: &mut Context) { self.root = root; - self.expanded_deps = None; + self.project = None; + self.profiles.clear(); + self.selected_profiles.clear(); + self.selected_module = None; self.dependencies.clear(); - self.dep_status = MavenStatus::Idle; + self.dep_status.clear(); self.reload(cx); } pub fn refresh(&mut self, cx: &mut Context) { - self.expanded_deps = None; + self.project = None; + self.profiles.clear(); + self.selected_profiles.clear(); self.dependencies.clear(); - self.dep_status = MavenStatus::Idle; + self.dep_status.clear(); self.reload(cx); } pub fn has_projects(&self) -> bool { - !self.poms.is_empty() + self.project.is_some() || !self.poms.is_empty() } - /// 重扫 pom 列表并默认展开全部 pom 节点与 Lifecycle 组。 + /// 全部折叠(对齐 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(); @@ -277,9 +456,10 @@ impl MavenView { } /// 本地重扫 + core `maven.scan` 拿模块结构: - /// payload `{root, paths}`(相对 pom 路径),返回 `MavenScanResponse` - ///(`relativePath` / `artifactId` / `modules` / `profiles` …)或 `null` - ///(未检出工程)。失败只落 `project_status`,导航照常展示本地 pom。 + /// 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; @@ -301,14 +481,27 @@ impl MavenView { } match result { Ok(data) => { - view.project_name = data - .get("artifactId") - .and_then(|value| value.as_str()) - .map(str::to_string); + 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_name = None; + view.project = None; + view.profiles.clear(); + view.selected_profiles.clear(); view.project_status = MavenStatus::Failed(err); } } @@ -325,43 +518,127 @@ impl MavenView { cx.notify(); } - /// 依赖组一次只展开一个:收起直接关,展开则触发依赖加载。 - fn toggle_deps(&mut self, pom: &str, cx: &mut Context) { - if self.expanded_deps.as_deref() == Some(pom) { - self.expanded_deps = None; + /// 依赖组展开即加载、收起即保留缓存(对齐 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 { - self.expanded_deps = Some(pom.to_string()); - self.load_deps(pom, cx); + 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: "."}, module}`, + /// (payload `{root, context: {version: 1, reactorPath: ".", profiles, skipTests}, module}`, /// 返回 `MavenLaunchPlanResponse`),再 `maven.dependencies` /// (payload `{modulePath, output}`,返回 `{modulePath, dependencies[]}`) - /// 解析依赖列表。Linux 侧无 Maven 进程宿主,`output` 为空即返回空列表; + /// 递归解析依赖树。Linux 侧无 Maven 进程宿主,`output` 为空即返回空列表; /// 真实树需在终端运行 `dependency:tree`,失败文案直接展示。 - fn load_deps(&mut self, pom: &str, cx: &mut Context) { - self.dep_seq += 1; - let seq = self.dep_seq; - self.dep_status = MavenStatus::Loading; - self.dependencies.clear(); + /// 每个模块独立序号与 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 = module_path_for_pom(&self.root, pom); + 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::( + .execute_with_operation_id::( &cx, "maven.dependencyPlan", serde_json::json!({ "root": root, - "context": { "version": 1, "reactorPath": "." }, + "context": { + "version": 1, + "reactorPath": ".", + "profiles": profiles, + "skipTests": skip_tests, + }, "module": module, }), + Some(op), ) .await; let outcome: Result, String> = match plan { @@ -379,16 +656,18 @@ impl MavenView { Err(err) => Err(err), }; let _ = this.update(cx, |view, cx| { - if view.dep_seq != seq { + if view.dep_seq.get(&module).copied().unwrap_or(0) != seq { return; } + view.dep_ops.remove(&module); match outcome { Ok(deps) => { - view.dependencies = deps; - view.dep_status = MavenStatus::Ready; + view.dependencies.insert(module.clone(), deps); + view.dep_status.insert(module.clone(), MavenStatus::Ready); } Err(err) => { - view.dep_status = MavenStatus::Failed(err); + view.dep_status + .insert(module.clone(), MavenStatus::Failed(err)); } } cx.notify(); @@ -412,6 +691,44 @@ impl MavenView { } } +/// 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。 @@ -428,280 +745,183 @@ impl Render for MavenView { )); } - // 快照后构建行,避免在 `.children` 闭包里同时借用 self 与 cx。 - let root = self.root.clone(); - let poms = self.poms.clone(); + // 快照后构建行,避免在闭包里同时借用 self 与 cx。 let expanded = self.expanded.clone(); - let selected = self.selected.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 project_name = self.project_name.clone(); - let expanded_deps = self.expanded_deps.clone(); let dep_status = self.dep_status.clone(); let dependencies = self.dependencies.clone(); let custom_goal = self.custom_goal.clone(); - let has_projects = !poms.is_empty(); + let poms = self.poms.clone(); - let title_text = match &project_name { - Some(name) => format!("{} · {name}", crate::i18n::menu_text(cx, "maven.title")), + 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, "正在扫描 Maven 项目…".to_string())), + MavenStatus::Loading => Some(( + false, + crate::i18n::menu_text(cx, "maven.scanning").to_string(), + )), MavenStatus::Failed(err) => Some((true, err.clone())), - MavenStatus::Idle | MavenStatus::Ready => None, + MavenStatus::Idle | MavenStatus::Ready | MavenStatus::Cancelled => None, }; - // 自定义 goal 的执行目标:选中 phase 行归属的 pom,否则首个 pom。 - let goal_target = selected_pom(&selected).or_else(|| poms.first().cloned()); - - let mut rows = Vec::new(); - for pom in &poms { - let pom_expanded = expanded.contains(pom); - let chevron = if pom_expanded { - IconName::ChevronDown - } else { - IconName::ChevronRight - }; - let display = display_pom_path(&root, pom); - rows.push( - h_flex() - .id(pom.clone()) - .h(px(24.0)) - .w_full() - .items_center() - .cursor_pointer() - .rounded_sm() - .pl(px(6.0)) - .pr_2() - .gap_1p5() - .text_xs() - .text_color(ThemeColors::text_primary()) - .hover(|h| h.bg(ThemeColors::bg_tab_hover())) - .child( - Icon::new(chevron) - .size(px(12.0)) - .text_color(ThemeColors::text_muted()), - ) - .child( - Icon::new(IconName::Box) - .size(px(14.0)) - .text_color(ThemeColors::accent_blue()), - ) - .child(div().flex_1().truncate().child(display)) - .on_click(cx.listener({ - let pom = pom.clone(); - move |this, _event, _window, cx| { - this.toggle_node(&pom, cx); - } - })) - .into_any_element(), - ); - if !pom_expanded { - continue; - } + let has_project = project.is_some(); - let group_key = lifecycle_key(pom); - let group_expanded = expanded.contains(&group_key); - let group_chevron = if group_expanded { - IconName::ChevronDown - } else { - IconName::ChevronRight - }; - rows.push( - h_flex() - .id(group_key.clone()) - .h(px(24.0)) - .w_full() - .items_center() - .cursor_pointer() - .rounded_sm() - .pl(px(18.0)) - .pr_2() - .gap_1p5() - .text_xs() - .text_color(ThemeColors::text_primary()) - .hover(|h| h.bg(ThemeColors::bg_tab_hover())) - .child( - Icon::new(group_chevron) - .size(px(12.0)) - .text_color(ThemeColors::text_muted()), - ) - .child( - Icon::new(IconName::Package) - .size(px(14.0)) - .text_color(ThemeColors::accent_blue()), - ) - .child( - div() - .flex_1() - .truncate() - .child(crate::i18n::menu_text(cx, "maven.lifecycle")), - ) - .on_click(cx.listener({ - let group_key = group_key.clone(); - move |this, _event, _window, cx| { - this.toggle_node(&group_key, cx); - } - })) - .into_any_element(), - ); - if group_expanded { - for phase in MAVEN_LIFECYCLE_PHASES { - let row_id = phase_id(pom, phase); - let is_selected = selected.as_deref() == Some(row_id.as_str()); - rows.push( - h_flex() - .id(row_id.clone()) - .h(px(24.0)) - .w_full() - .items_center() - .cursor_pointer() - .rounded_sm() - .pl(px(30.0)) - .pr_2() - .gap_1p5() - .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(ThemeColors::text_muted()), - ) - .child(div().flex_1().truncate().child(phase)) - .on_click(cx.listener({ - let pom = pom.clone(); - let phase = phase.to_string(); - move |this, _event, _window, cx| { - this.selected = Some(phase_id(&pom, &phase)); - cx.emit(MavenEvent::RunGoal { - pom_path: pom.clone(), - phase: phase.clone(), - }); - cx.notify(); - } - })) - .into_any_element(), - ); + // 当前选中 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 + }; - // 依赖组:一次只展开一个模块,点击触发 dependencyPlan + dependencies。 - let deps_open = expanded_deps.as_deref() == Some(pom.as_str()); - let deps_chevron = if deps_open { - IconName::ChevronDown - } else { - IconName::ChevronRight - }; - rows.push( - h_flex() - .id(deps_key(pom)) - .h(px(24.0)) - .w_full() - .items_center() - .cursor_pointer() - .rounded_sm() - .pl(px(18.0)) - .pr_2() - .gap_1p5() - .text_xs() - .text_color(ThemeColors::text_primary()) - .hover(|h| h.bg(ThemeColors::bg_tab_hover())) - .child( - Icon::new(deps_chevron) - .size(px(12.0)) - .text_color(ThemeColors::text_muted()), - ) - .child( - Icon::new(IconName::Package) - .size(px(14.0)) - .text_color(ThemeColors::accent_blue()), - ) - .child(div().flex_1().truncate().child("依赖")) - .on_click(cx.listener({ - let pom = pom.clone(); - move |this, _event, _window, cx| { - this.toggle_deps(&pom, cx); - } - })) - .into_any_element(), - ); - if deps_open { - match &dep_status { - MavenStatus::Loading => { - rows.push( - div() - .w_full() - .pl(px(30.0)) - .pr_2() - .py_1() - .text_xs() - .text_color(ThemeColors::text_muted()) - .child("正在加载依赖…") - .into_any_element(), - ); - } - MavenStatus::Failed(err) => { - rows.push( - div() - .w_full() - .pl(px(30.0)) - .pr_2() - .py_1() - .text_xs() - .text_color(ThemeColors::accent_red()) - .child(err.clone()) - .into_any_element(), - ); - } - MavenStatus::Idle | MavenStatus::Ready if dependencies.is_empty() => { - rows.push( - div() - .w_full() - .pl(px(30.0)) - .pr_2() - .py_1() - .text_xs() - .text_color(ThemeColors::text_muted()) - .child("暂无依赖数据") - .into_any_element(), - ); - } - MavenStatus::Idle | MavenStatus::Ready => { - for (index, dep) in dependencies.iter().enumerate() { - let label = format!( - "{}:{}:{} [{}]", - dep.group, dep.artifact, dep.version, dep.scope - ); - rows.push( - h_flex() - .id(format!("{pom}#dep#{index}")) - .w_full() - .items_center() - .rounded_sm() - .pl(px(30.0)) - .pr_2() - .py_1() - .gap_1p5() - .text_xs() - .text_color(ThemeColors::text_primary()) - .hover(|h| h.bg(ThemeColors::bg_tab_hover())) - .child(div().flex_1().truncate().child(label)) - .into_any_element(), - ); - } - } + 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 goal_target_for_run = goal_target.clone(); + let run_scope = (scope_pom.clone(), scope_target.clone()); + let run_profiles = scope_profiles.clone(); v_flex() .size_full() .bg(ThemeColors::bg_sidebar()) @@ -719,35 +939,111 @@ impl Render for MavenView { .px_3() .child( div() + .flex_1() + .truncate() .text_xs() .font_weight(FontWeight::BOLD) .text_color(ThemeColors::text_muted()) .child(title_text), ) .child( - h_flex() - .items_center() - .gap_1() - .child( - Button::new("maven-refresh") - .small() - .ghost() - .icon(IconName::RotateCw) - .tooltip(crate::i18n::menu_text(cx, "ui.refresh")) - .on_click(cx.listener(|this, _event, _window, cx| { - this.refresh(cx); - })), - ) - .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); - })), - ), + 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)| { @@ -767,25 +1063,7 @@ impl Render for MavenView { .child(text), ) }) - .child(if has_projects { - div() - .flex_1() - .w_full() - .overflow_y_scrollbar() - .children(rows) - .into_any_element() - } else { - div() - .flex_1() - .w_full() - .flex() - .items_center() - .justify_center() - .text_xs() - .text_color(ThemeColors::text_muted()) - .child(crate::i18n::menu_text(cx, "maven.noProjects")) - .into_any_element() - }) + .child(body_element) .child( h_flex() .w_full() @@ -804,24 +1082,631 @@ impl Render for MavenView { .small() .primary() .icon(IconName::Play) - .tooltip("执行自定义 goal") - .disabled( - custom_goal.trim().is_empty() || goal_target_for_run.is_none(), - ) - .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(); + .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; } - if let Some(pom) = goal_target_for_run.clone() { - cx.emit(MavenEvent::RunGoal { - pom_path: pom, - phase: goal, - }); - } + 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/view.rs b/linux/src/workbench/view.rs index f269da00d..b53505ebf 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -362,12 +362,17 @@ impl WorkbenchView { }, ); - // 4b. 订阅 Maven 视图事件(执行目标、关闭工具窗口) - // 对齐 Tauri:目标走底部 Maven 页受管进程,不进交互终端。 + // 4b. 订阅 Maven 视图事件(执行目标走底部 Maven 页、依赖开 pom、设置对话框、关闭工具窗口) let sub_maven = cx.subscribe(&maven, |this, _maven, event: &MavenEvent, cx| match event { - MavenEvent::RunGoal { pom_path, phase } => { + MavenEvent::RunGoal { + pom_path, + phase, + target, + profiles, + skip_tests, + } => { let _ = this.bottom_panel.update(cx, |bp, cx| { - bp.run_maven_goal(pom_path, phase, cx); + bp.run_maven_goal(pom_path, phase, target, profiles, *skip_tests, cx); }); let has_run = this.bottom_panel.read(cx).has_maven_run(); let _ = this.activity_rail.update(cx, |r, cx| { @@ -375,6 +380,12 @@ impl WorkbenchView { }); cx.notify(); } + MavenEvent::OpenFile(path) => { + this.open_file(path, cx); + } + MavenEvent::OpenSettings => { + this.open_settings(cx); + } MavenEvent::Close => { this.right_tool = None; cx.notify(); From f8011570a904d712271be0e1bb721759adb85f3c Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 01:11:35 +0800 Subject: [PATCH 42/59] =?UTF-8?q?fix(linux):=20Maven=20=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E6=89=93=E5=BC=80=E9=A1=B9=E7=9B=AE=20JDK=20?= =?UTF-8?q?=E4=B8=8E=20Maven=20=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- linux/src/workbench/view.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/linux/src/workbench/view.rs b/linux/src/workbench/view.rs index b53505ebf..c1bd5cc96 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -384,7 +384,12 @@ impl WorkbenchView { this.open_file(path, cx); } MavenEvent::OpenSettings => { - this.open_settings(cx); + // 对齐 Tauri `openSettingsDialog("project")`:定位到项目 JDK 与 Maven 页。 + this.show_settings_dialog = true; + let _ = this.settings_dialog.update(cx, |d, cx| { + d.set_category(SettingsCategory::Project, cx); + }); + cx.notify(); } MavenEvent::Close => { this.right_tool = None; From 1f5d3682dcbec16334b0e3bc487f81bd52814815 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 01:13:34 +0800 Subject: [PATCH 43/59] =?UTF-8?q?fix(linux):=20=E6=A0=87=E7=AD=BE=E5=85=B3?= =?UTF-8?q?=E9=97=AD=E6=8C=89=E9=92=AE=E5=AF=B9=E9=BD=90=20Tauri=20?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- linux/src/workbench/editor.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/linux/src/workbench/editor.rs b/linux/src/workbench/editor.rs index 32b542776..282778855 100644 --- a/linux/src/workbench/editor.rs +++ b/linux/src/workbench/editor.rs @@ -1088,9 +1088,18 @@ impl Render for EditorView { 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() @@ -1158,6 +1167,10 @@ impl Render for EditorView { .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()) From eff87ee97badfbc6a0d46f996088d9af9df4910a Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 01:21:37 +0800 Subject: [PATCH 44/59] =?UTF-8?q?fix(linux):=20=E5=BA=95=E9=83=A8=E7=AA=97?= =?UTF-8?q?=E6=A0=BC=E6=94=B9=E5=90=84=E7=AA=97=E6=A0=BC=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E5=A4=B4=EF=BC=8C=E5=AF=B9=E9=BD=90=20Tauri?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删共享标签条,切换只走左侧活动栏 - Run 头:运行/停止+重扫+清空+最小化;Maven 头:停止+重跑+清空+最小化 - 终端/诊断/GitLog 头:标题+最小化,保留清空/刷新 - 进程 framing 行与清空语义全部走 i18n 键 --- linux/src/i18n.rs | 20 + linux/src/workbench/bottom_panel.rs | 714 ++++++++++++++-------------- 2 files changed, 387 insertions(+), 347 deletions(-) diff --git a/linux/src/i18n.rs b/linux/src/i18n.rs index 3dcd560a8..91804dc11 100644 --- a/linux/src/i18n.rs +++ b/linux/src/i18n.rs @@ -137,6 +137,26 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { "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"), diff --git a/linux/src/workbench/bottom_panel.rs b/linux/src/workbench/bottom_panel.rs index 1d2aec2f4..b49843ac1 100644 --- a/linux/src/workbench/bottom_panel.rs +++ b/linux/src/workbench/bottom_panel.rs @@ -6,8 +6,8 @@ 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, AnyElement, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement, - ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Window, + div, px, AnyElement, AppContext as _, Context, Entity, FontWeight, InteractiveElement as _, + IntoElement, ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Window, }; use crate::core::CoreClient; @@ -59,6 +59,36 @@ struct RunStep { cwd: String, } +/// 进程 framing 行文案(`run_steps_blocking` 无 `cx`,调用方预取传入)。 +#[derive(Debug, Clone)] +struct StepText { + start_failed: String, + exit_code: String, + exited: String, + finished: String, +} + +impl StepText { + fn load(cx: &gpui_kit::App) -> Self { + Self { + start_failed: crate::i18n::menu_text(cx, "run.startFailed").to_string(), + exit_code: crate::i18n::menu_text(cx, "run.exitCode").to_string(), + exited: crate::i18n::menu_text(cx, "run.exited").to_string(), + finished: crate::i18n::menu_text(cx, "run.finished").to_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; @@ -115,6 +145,8 @@ pub struct BottomPanelView { maven_child: Arc>>, /// Maven 启动序号,丢弃过期 `launchPlan` 结果。 maven_seq: u64, + /// 上次 Maven 目标参数(头部重跑按钮回放)。 + last_maven_goal: Option, /// reload 序号,丢弃过期探测结果。 run_seq: u64, /// 最近一次加载的 Git 提交记录。 @@ -146,6 +178,7 @@ impl BottomPanelView { maven_running: false, maven_child: Arc::new(Mutex::new(None)), maven_seq: 0, + last_maven_goal: None, client: CoreClient::new(), run_child: Arc::new(Mutex::new(None)), run_seq: 0, @@ -280,10 +313,18 @@ impl BottomPanelView { return; }; self.run_running = true; - push_run_line(&mut self.run_output, format!("开始运行:{}", item.name)); + push_run_line( + &mut self.run_output, + format!( + "{}:{}", + crate::i18n::menu_text(cx, "run.starting"), + item.name + ), + ); cx.notify(); let (tx, rx) = mpsc::channel::(); let child_slot = self.run_child.clone(); + let text = StepText::load(cx); let client = self.client.clone(); let root = self.working_dir.clone(); cx.spawn(async move |this, cx| { @@ -302,7 +343,10 @@ impl BottomPanelView { let started = this.update(cx, |view, cx| { if steps.is_empty() { view.run_running = false; - push_run_line(&mut view.run_output, "无可执行步骤".to_string()); + push_run_line( + &mut view.run_output, + crate::i18n::menu_text(cx, "run.noSteps").to_string(), + ); cx.notify(); return false; } @@ -315,7 +359,7 @@ impl BottomPanelView { if !matches!(started, Ok(true)) { return; } - std::thread::spawn(move || run_steps_blocking(steps, child_slot, tx)); + std::thread::spawn(move || run_steps_blocking(steps, child_slot, text, tx)); // 输出泵:每行经 background 线程阻塞收,再回到主线程落盘展示。 let rx = Arc::new(Mutex::new(rx)); loop { @@ -366,11 +410,17 @@ impl BottomPanelView { } } if self.run_running { - push_run_line(&mut self.run_output, "已发送停止信号…".to_string()); + push_run_line( + &mut self.run_output, + crate::i18n::menu_text(cx, "run.stopping").to_string(), + ); stopped = true; } if self.maven_running { - push_run_line(&mut self.maven_output, "已发送停止信号…".to_string()); + push_run_line( + &mut self.maven_output, + crate::i18n::menu_text(cx, "run.stopping").to_string(), + ); stopped = true; } if stopped { @@ -411,6 +461,13 @@ impl BottomPanelView { 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.clone()); @@ -422,6 +479,7 @@ impl BottomPanelView { let root = self.working_dir.clone(); let module = maven_module_for_pom(&self.working_dir, pom_path); let profiles = profiles.to_vec(); + let text = StepText::load(cx); let child_slot = self.maven_child.clone(); cx.spawn(async move |this, cx| { let plan = client @@ -458,7 +516,7 @@ impl BottomPanelView { } let steps = vec![RunStep { program, args, cwd }]; let (tx, rx) = mpsc::channel::(); - std::thread::spawn(move || run_steps_blocking(steps, child_slot, tx)); + std::thread::spawn(move || run_steps_blocking(steps, child_slot, text, tx)); let rx = Arc::new(Mutex::new(rx)); loop { let slot = rx.clone(); @@ -501,6 +559,24 @@ impl BottomPanelView { .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, + ); + } + } + /// 当前选中项(无选中或已失效时回退首项)。 fn selected_run_item(&self) -> Option { self.selected_run_config @@ -520,51 +596,6 @@ impl BottomPanelView { } } - /// Run 面板状态行文案。 - fn run_state_text(&self, cx: &gpui_kit::App) -> String { - let zh = crate::i18n::is_zh(cx); - match &self.run_state { - RunProjectState::Missing => { - if zh { - "无可运行配置".to_string() - } else { - "No runnable configurations".to_string() - } - } - RunProjectState::Loading => { - if zh { - "加载中…".to_string() - } else { - "Loading…".to_string() - } - } - RunProjectState::Ready => { - if self.run_running { - if zh { - "运行中".to_string() - } else { - "Running".to_string() - } - } else if zh { - format!("就绪 · {} 个配置", self.run_configs.len()) - } else { - format!("Ready · {} configurations", self.run_configs.len()) - } - } - RunProjectState::Failed(err) => { - if err.is_empty() { - if zh { - "加载失败".to_string() - } else { - "Failed to load".to_string() - } - } else { - err.clone() - } - } - } - } - /// 重新加载 Git 提交记录。core 的 `git.historyPage` 需经宿主异步接线, /// 这里用 `git -C log --oneline -50` 同步退化实现, /// 解析 hash + 首行 message 只读展示。 @@ -586,44 +617,192 @@ impl BottomPanelView { /// 当前仅保留调用点可编译,不存储不展示。 pub fn append_log(&mut self, _log: String, _cx: &mut Context) {} - fn render_tab_button( + /// 窗格通用头部:图标 + 标题 + 状态区 + 操作按钮 + 最小化, + /// 对齐 Tauri 各窗格自带按钮头(底部无标签切换条,切换只走左侧活动栏)。 + fn render_pane_header( &self, - id: &'static str, icon: IconName, - label: String, - is_active: bool, - tab: BottomTab, + title: String, + status: Option, + buttons: Vec, cx: &mut Context, - ) -> impl IntoElement { + ) -> AnyElement { h_flex() - .id(id) - .items_center() - .gap_1p5() - .px_3() .h(px(30.0)) - .cursor_pointer() - .text_xs() - .when(is_active, |btn| { - btn.bg(ThemeColors::bg_bottom_panel()) + .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()) - .border_b_2() - .border_color(ThemeColors::accent_blue()) - }) - .when(!is_active, |btn| { - btn.text_color(ThemeColors::text_muted()).hover(|h| { - h.bg(ThemeColors::bg_tab_hover()) - .text_color(ThemeColors::text_primary()) - }) - }) - .child(Icon::new(icon).size(px(13.0)).text_color(if is_active { - ThemeColors::accent_blue() - } else { - ThemeColors::text_muted() - })) - .child(label) - .on_click(cx.listener(move |this, _event, _window, cx| { - this.set_tab(tab, cx); + .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); + self.render_pane_header( + IconName::Play, + title, + status, + 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), + ), + 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(); + }, + ), + ], + 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 面板:Tauri RunPane 子集——顶部状态行(工程名+状态+运行/停止+ @@ -689,15 +868,6 @@ impl BottomPanelView { } } - let running = self.run_running; - let run_label = if running { - run_ui_text(cx, "停止", "Stop") - } else { - run_ui_text(cx, "运行", "Run") - }; - let can_run = !matches!(self.run_state, RunProjectState::Loading); - let status = format!("{} · {}", self.run_project_name, self.run_state_text(cx)); - let rows: Vec = self .run_configs .iter() @@ -761,7 +931,7 @@ impl BottomPanelView { let output: Vec = if self.run_output.is_empty() { vec![div() .text_color(ThemeColors::text_muted()) - .child(run_ui_text(cx, "暂无输出", "No output")) + .child(crate::i18n::menu_text(cx, "run.emptyOutput")) .into_any_element()] } else { self.run_output[start..] @@ -772,55 +942,6 @@ impl BottomPanelView { v_flex() .size_full() - .child( - h_flex() - .flex_shrink_0() - .h(px(30.0)) - .w_full() - .items_center() - .gap_2() - .px_3() - .border_b_1() - .border_color(ThemeColors::border()) - .child( - div() - .flex_1() - .truncate() - .text_xs() - .text_color(ThemeColors::text_primary()) - .child(status), - ) - .child( - Button::new("run-toggle") - .small() - .primary() - .disabled(!can_run) - .label(run_label) - .on_click(cx.listener(|this, _event, _window, cx| { - this.run_selected_config(cx); - })), - ) - .child( - Button::new("run-rescan") - .small() - .ghost() - .disabled(running) - .label(run_ui_text(cx, "重扫", "Rescan")) - .on_click(cx.listener(|this, _event, _window, cx| { - this.reload_run_project(cx); - })), - ) - .child( - Button::new("run-clear-output") - .small() - .ghost() - .label(run_ui_text(cx, "清空输出", "Clear")) - .on_click(cx.listener(|this, _event, _window, cx| { - this.run_output.clear(); - cx.notify(); - })), - ), - ) .child( h_flex() .flex_1() @@ -852,22 +973,14 @@ impl BottomPanelView { .into_any_element() } - /// Maven 面板:对齐 Tauri Maven 页——顶部任务标题+运行状态+停止/清空, - /// 输出区首行 `$ mvn …`,流式追加进程输出。 + /// Maven 面板体:输出区(首行 `$ mvn …`,流式追加);头部由 `render_maven_header` 负责。 fn render_maven_panel(&self, cx: &mut Context) -> AnyElement { - let status = if self.maven_running { - run_ui_text(cx, "运行中…", "Running…") - } else if let Some(title) = self.maven_title.clone() { - title - } else { - run_ui_text(cx, "尚未运行 Maven 目标", "No Maven goal has run yet") - }; 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(run_ui_text(cx, "暂无输出", "No output")) + .child(crate::i18n::menu_text(cx, "run.emptyOutput")) .into_any_element()] } else { self.maven_output[start..] @@ -875,59 +988,16 @@ impl BottomPanelView { .map(|line| div().child(line.clone()).into_any_element()) .collect() }; - v_flex() - .size_full() - .child( - h_flex() - .flex_shrink_0() - .h(px(30.0)) - .w_full() - .items_center() - .gap_2() - .px_3() - .border_b_1() - .border_color(ThemeColors::border()) - .child( - div() - .flex_1() - .truncate() - .text_xs() - .text_color(ThemeColors::text_primary()) - .child(status), - ) - .child( - Button::new("maven-stop") - .small() - .primary() - .disabled(!self.maven_running) - .label(run_ui_text(cx, "停止", "Stop")) - .on_click(cx.listener(|this, _event, _window, cx| { - this.stop_running(cx); - })), - ) - .child( - Button::new("maven-clear-output") - .small() - .ghost() - .label(run_ui_text(cx, "清空输出", "Clear")) - .on_click(cx.listener(|this, _event, _window, cx| { - this.maven_output.clear(); - cx.notify(); - })), - ), - ) - .child( - 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), - ) + 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() } @@ -1007,149 +1077,98 @@ impl Render for BottomPanelView { 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.notify(); + }, + )], + cx, + ), + div() + .size_full() + .p_3() + .overflow_y_scrollbar() + .text_xs() + .text_color(ThemeColors::text_muted()) + .child(if self.diagnostics.is_empty() { + crate::i18n::menu_text(cx, "diagnostics.empty").to_string() + } else { + format!( + "{} ({})", + crate::i18n::menu_text(cx, "workbench.diagnostics"), + self.diagnostics.len() + ) + }) + .into_any_element(), + ), + 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( - // 顶部 Tab 切换栏(高 30px):仅保留 Tauri 存在的 Terminal / Diagnostics, - // 自创的 Output 页签已去掉。 - h_flex() - .h(px(30.0)) - .w_full() - .bg(ThemeColors::bg_tab_bar()) - .border_b_1() - .border_color(ThemeColors::border()) - .items_center() - .justify_between() - .px_2() - .child( - h_flex() - .items_center() - .gap_1() - .child(self.render_tab_button( - "tab-terminal", - IconName::Terminal, - crate::i18n::menu_text(cx, "workbench.terminal").to_string(), - self.active_tab == BottomTab::Terminal, - BottomTab::Terminal, - cx, - )) - .child(self.render_tab_button( - "tab-run", - IconName::Play, - crate::i18n::menu_text(cx, "workbench.run").to_string(), - self.active_tab == BottomTab::Run, - BottomTab::Run, - cx, - )) - .child(self.render_tab_button( - "tab-maven", - IconName::Box, - crate::i18n::menu_text(cx, "maven.title").to_string(), - self.active_tab == BottomTab::Maven, - BottomTab::Maven, - cx, - )) - .child(self.render_tab_button( - "tab-diagnostics", - IconName::TriangleAlert, - format!( - "{} ({})", - crate::i18n::menu_text(cx, "workbench.diagnostics"), - self.diagnostics.len() - ), - self.active_tab == BottomTab::Diagnostics, - BottomTab::Diagnostics, - cx, - )) - .child(self.render_tab_button( - "tab-gitlog", - IconName::GitGraph, - crate::i18n::menu_text(cx, "workbench.gitLog").to_string(), - self.active_tab == BottomTab::GitLog, - BottomTab::GitLog, - cx, - )), - ) - .child( - h_flex() - .items_center() - .gap_1() - .child( - Button::new("clear-panel") - .small() - .ghost() - .icon(IconName::Trash) - .tooltip(crate::i18n::menu_text(cx, "ui.clear")) - .on_click(cx.listener(|this, _event, _window, cx| { - match this.active_tab { - BottomTab::Terminal => { - let _ = - this.terminal.update(cx, |t, cx| t.clear(cx)); - } - BottomTab::Run => { - this.run_history.clear(); - this.run_output.clear(); - cx.notify(); - } - BottomTab::Maven => { - this.maven_output.clear(); - cx.notify(); - } - BottomTab::Diagnostics => { - this.diagnostics.clear(); - cx.notify(); - } - // GitLog 无可清空的输出,该按钮退化为手动刷新。 - BottomTab::GitLog => { - this.refresh_git_log(cx); - } - } - })), - ) - .child( - Button::new("collapse-panel") - .small() - .ghost() - .icon(IconName::ChevronDown) - .tooltip(crate::i18n::menu_text(cx, "ui.close")) - .on_click(cx.listener(|this, _event, _window, cx| { - this.toggle_collapsed(cx); - })), - ), - ), - ) - .child( - // 内容区域根据 Tab 切换 - div().flex_1().w_full().child(match self.active_tab { - BottomTab::Terminal => div() - .size_full() - .child(self.terminal.clone()) - .into_any_element(), - BottomTab::Run => self.render_run_panel(cx), - BottomTab::Maven => self.render_maven_panel(cx), - BottomTab::Diagnostics => div() - .size_full() - .p_3() - .overflow_y_scrollbar() - .text_xs() - .text_color(ThemeColors::text_muted()) - .child(if self.diagnostics.is_empty() { - crate::i18n::menu_text(cx, "diagnostics.empty").to_string() - } else { - format!( - "{} ({})", - crate::i18n::menu_text(cx, "workbench.diagnostics"), - self.diagnostics.len() - ) - }) - .into_any_element(), - BottomTab::GitLog => self.render_git_log_panel(cx), - }), - ) + .child(header) + .child(div().flex_1().w_full().min_h_0().child(body)) } } @@ -1634,6 +1653,7 @@ fn pick_npm_script(root: &str) -> String { fn run_steps_blocking( steps: Vec, child_slot: Arc>>, + text: StepText, tx: mpsc::Sender, ) { use std::io::BufRead as _; @@ -1648,7 +1668,7 @@ fn run_steps_blocking( { Ok(child) => child, Err(err) => { - let _ = tx.send(format!("启动失败:{err}")); + let _ = tx.send(format!("{}:{err}", text.start_failed)); return; } }; @@ -1701,14 +1721,14 @@ fn run_steps_blocking( } match status { Some(status) if status.success() => { - let _ = tx.send("退出码:0".to_string()); + let _ = tx.send(format!("{}:0", text.exit_code)); } Some(status) => { - let _ = tx.send(format!("进程已退出:{status}")); + let _ = tx.send(format!("{}:{status}", text.exited)); return; } None => { - let _ = tx.send("运行已结束".to_string()); + let _ = tx.send(text.finished.clone()); return; } } From 2f8e47ae8166e17e126c235b95d1de480d43fbe9 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 01:30:10 +0800 Subject: [PATCH 45/59] =?UTF-8?q?fix(linux):=20=E7=BB=88=E7=AB=AF=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E4=B8=8E=E8=BE=93=E5=85=A5=E5=AF=B9=E9=BD=90=E5=8F=AF?= =?UTF-8?q?=E7=94=A8=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删自带控制栏(与窗格头重复),输出区占满 - 输出改 channel 主线程泵,实时出现;ANSI 剥离 + 回车覆盖 - 输入条换真实单行输入,回车/按钮发送整行 - 文案全部走 terminal.* 键 --- linux/src/i18n.rs | 7 + linux/src/workbench/terminal.rs | 361 +++++++++++++++++++++++--------- 2 files changed, 270 insertions(+), 98 deletions(-) diff --git a/linux/src/i18n.rs b/linux/src/i18n.rs index 91804dc11..086a2f227 100644 --- a/linux/src/i18n.rs +++ b/linux/src/i18n.rs @@ -160,6 +160,13 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { "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"), + "terminal.execute" => ("执行", "Execute"), "workbench.settings" => ("设置", "Settings"), "workbench.sourceControl" => ("源代码管理", "Source Control"), "fileExplorer.searchFiles" => ("搜索文件", "Search files"), diff --git a/linux/src/workbench/terminal.rs b/linux/src/workbench/terminal.rs index f5ae3271e..3f13c63e6 100644 --- a/linux/src/workbench/terminal.rs +++ b/linux/src/workbench/terminal.rs @@ -1,18 +1,78 @@ 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, Sizable as _}; use gpui_kit::{ - div, px, Context, InteractiveElement as _, IntoElement, ParentElement as _, Render, - Styled as _, Window, + div, px, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement, + ParentElement as _, Render, Styled as _, Subscription, Window, }; use portable_pty::{native_pty_system, CommandBuilder, PtySize}; use std::io::{Read, Write}; -use std::sync::{Arc, Mutex}; +use std::sync::{mpsc, Arc, Mutex}; use std::thread; use crate::settings; use crate::theme::ThemeColors; +/// 输出行数上限。 +const MAX_TERMINAL_LINES: usize = 1000; + +/// 未换行缓冲上限(字符),超出保留末尾,避免无换物流撑爆内存。 +const MAX_PENDING_CHARS: usize = 8192; +const PENDING_KEEP_CHARS: usize = 4096; + +/// 剥离 ANSI 转义序列与无用控制字符(保留 `\n` `\r` `\t` 交由换行逻辑处理)。 +/// 无 xterm 网格,按纯文本展示是既定适配(右键菜单/复制等同理缺失)。 +fn strip_ansi(input: &str) -> String { + let mut out = String::with_capacity(input.len()); + let mut chars = input.chars().peekable(); + while let Some(ch) = chars.next() { + if ch == '\x1b' { + match chars.peek() { + // CSI:`ESC [` … 最终字节 `@`..=`~` + Some('[') => { + chars.next(); + loop { + match chars.next() { + None => break, + Some(c) if ('@'..='~').contains(&c) => break, + Some(_) => {} + } + } + } + // OSC:`ESC ]` … 以 BEL 或 `ESC \` 结束 + Some(']') => { + chars.next(); + let mut prev_esc = false; + loop { + match chars.next() { + None => break, + Some('\x07') => break, + Some('\x1b') => prev_esc = true, + Some('\\') if prev_esc => break, + Some(_) => prev_esc = false, + } + } + } + // 字符集选择等两字符序列 + Some('(') | Some(')') | Some('#') => { + chars.next(); + chars.next(); + } + Some(_) => { + chars.next(); + } + None => {} + } + } else if ch.is_control() && ch != '\n' && ch != '\r' && ch != '\t' { + // 丢弃 BEL 等无用控制字符 + } else { + out.push(ch); + } + } + out +} + /// Linux 原生 PTY 会话 pub struct TerminalSession { writer: Arc>>, @@ -22,7 +82,16 @@ pub struct TerminalSession { } impl TerminalSession { - pub fn new(cols: u16, rows: u16, working_dir: &str, shell: &str) -> anyhow::Result { + /// 打开 PTY 会话并起输出线程:线程只透传原始字节块,主线程泵负责 + /// 解析换行/回车并 `notify`(输出实时出现,对齐 xterm 的流式展示)。 + /// 返回会话与输出 channel;`banner` 为首行提示(调用方按语言构造)。 + pub fn new( + cols: u16, + rows: u16, + working_dir: &str, + shell: &str, + banner: Option, + ) -> anyhow::Result<(Self, mpsc::Receiver)> { let pty_system = native_pty_system(); let pair = pty_system.openpty(PtySize { rows, @@ -43,48 +112,34 @@ impl TerminalSession { let mut reader = pair.master.try_clone_reader()?; let writer = pair.master.take_writer()?; - let output_lines = Arc::new(Mutex::new(vec![format!( - "Lithe Terminal (PTY Session: {shell})" - )])); - let output_lines_clone = Arc::clone(&output_lines); + let output_lines = Arc::new(Mutex::new( + banner.map(|line| vec![line]).unwrap_or_default(), + )); + let (tx, rx) = mpsc::channel::(); thread::spawn(move || { - let mut buf = [0u8; 1024]; - let mut line_accumulator = String::new(); - - while let Ok(n) = reader.read(&mut buf) { - if n == 0 { - break; - } - let chunk = String::from_utf8_lossy(&buf[..n]); - for ch in chunk.chars() { - if ch == '\n' { - let mut lines = output_lines_clone.lock().unwrap(); - lines.push(line_accumulator.clone()); - if lines.len() > 1000 { - lines.remove(0); + let mut buf = [0u8; 4096]; + loop { + match reader.read(&mut buf) { + Ok(0) | Err(_) => break, + Ok(n) => { + let chunk = String::from_utf8_lossy(&buf[..n]).into_owned(); + if tx.send(chunk).is_err() { + break; } - line_accumulator.clear(); - } else if ch == '\r' { - // ignore carriage return - } else { - line_accumulator.push(ch); - } - } - if !line_accumulator.is_empty() { - let mut lines = output_lines_clone.lock().unwrap(); - if let Some(last) = lines.last_mut() { - *last = line_accumulator.clone(); } } } }); - Ok(Self { - writer: Arc::new(Mutex::new(writer)), - output_lines, - input_buffer: Arc::new(Mutex::new(String::new())), - }) + Ok(( + Self { + writer: Arc::new(Mutex::new(writer)), + output_lines, + input_buffer: Arc::new(Mutex::new(String::new())), + }, + rx, + )) } pub fn write_input(&self, input: &str) -> anyhow::Result<()> { @@ -99,21 +154,113 @@ impl TerminalSession { } } -/// 终端视图组件 +/// 终端视图组件:输出区 + 可输入的命令行(回车/按钮发送整行)。 pub struct TerminalView { pub session: Option, - pub current_input: String, pub working_dir: String, + /// 未换行缓冲:跨字节块累积,`\r` 覆盖当前行内容。 + pending: String, + input: Option>, + _input_subscription: Option, + /// 回车发送后待清空输入框(render 内有 `window` 才可执行)。 + clear_pending: bool, } impl TerminalView { pub fn new(working_dir: String, cx: &mut Context) -> Self { - let session = TerminalSession::new(120, 30, &working_dir, &Self::resolve_shell(cx)).ok(); - - Self { - session, - current_input: String::new(), + let mut view = Self { + session: None, working_dir, + pending: String::new(), + input: None, + _input_subscription: None, + clear_pending: false, + }; + view.attach_session(cx); + view + } + + /// 打开 PTY 会话并起主线程输出泵(channel 收字节块 → 解析落行 → notify)。 + fn attach_session(&mut self, cx: &mut Context) { + let banner = crate::i18n::menu_text(cx, "terminal.session") + .replace("{shell}", &Self::resolve_shell(cx)); + match TerminalSession::new( + 120, + 30, + &self.working_dir, + &Self::resolve_shell(cx), + Some(banner), + ) { + Ok((session, rx)) => { + self.session = Some(session); + self.pending.clear(); + Self::spawn_pump(rx, cx); + } + Err(_) => { + self.session = None; + } + } + cx.notify(); + } + + /// 主线程输出泵:后台收字节块,回主线程解析。 + fn spawn_pump(rx: mpsc::Receiver, cx: &mut Context) { + let rx = Arc::new(Mutex::new(rx)); + cx.spawn(async move |this, cx| loop { + let slot = rx.clone(); + let next = cx + .background_executor() + .spawn(async move { + let guard = slot.lock().ok()?; + guard.recv().ok() + }) + .await; + match next { + Some(chunk) => { + if this + .update(cx, |view, cx| { + view.push_chunk(&chunk); + cx.notify(); + }) + .is_err() + { + break; + } + } + None => break, + } + }) + .detach(); + } + + /// 解析输出块:ANSI 剥离后按换行落行、`\r` 覆盖当前行。 + fn push_chunk(&mut self, chunk: &str) { + let text = strip_ansi(chunk); + let Some(session) = self.session.as_ref() else { + return; + }; + let Ok(mut lines) = session.output_lines.lock() else { + return; + }; + for piece in text.split_inclusive('\n') { + let (content, newline) = match piece.strip_suffix('\n') { + Some(content) => (content, true), + None => (piece, false), + }; + // `\r` 语义为回车覆盖:只保留最后一个 `\r` 之后的内容。 + let visible = content.rsplit('\r').next().unwrap_or(""); + self.pending.push_str(visible); + let count = self.pending.chars().count(); + if count > MAX_PENDING_CHARS { + let skip = count - PENDING_KEEP_CHARS; + self.pending = self.pending.chars().skip(skip).collect(); + } + if newline { + lines.push(std::mem::take(&mut self.pending)); + if lines.len() > MAX_TERMINAL_LINES { + lines.remove(0); + } + } } } @@ -151,7 +298,6 @@ impl TerminalView { full.push('\n'); let _ = session.write_input(&full); } - self.current_input.clear(); cx.notify(); } @@ -159,68 +305,85 @@ impl TerminalView { if let Some(session) = &self.session { if let Ok(mut lines) = session.output_lines.lock() { lines.clear(); - lines.push("Terminal cleared.".to_string()); + lines.push(crate::i18n::menu_text(cx, "terminal.cleared").to_string()); } } + self.pending.clear(); cx.notify(); } /// 用现有工作目录重建 PTY 会话(失败则置空并通知)。 pub fn respawn(&mut self, cx: &mut Context) { - self.session = - TerminalSession::new(120, 30, &self.working_dir, &Self::resolve_shell(cx)).ok(); - cx.notify(); + self.session = None; + self.attach_session(cx); + } + + /// 懒创建命令行输入框。 + fn ensure_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 } } impl Render for TerminalView { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let lines = self + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + // 命令行输入框懒创建;回车发送整行(`PressEnter`),发送后在本次 + // render 内清空(`set_value` 需 `window`,订阅回调里拿不到)。 + let input_entity = Self::ensure_input(&mut self.input, window, cx); + if self._input_subscription.is_none() { + let entity = input_entity.clone(); + self._input_subscription = Some(cx.subscribe( + &input_entity, + move |this: &mut Self, _, event: &InputEvent, cx| { + if matches!(event, InputEvent::PressEnter { .. }) { + let cmd = entity.read(cx).value().to_string(); + if !cmd.trim().is_empty() { + this.send_command(&cmd, cx); + this.clear_pending = true; + cx.notify(); + } + } + }, + )); + } + if self.clear_pending { + self.clear_pending = false; + let entity = input_entity.clone(); + entity.update(cx, |state, cx| { + state.set_value("", window, cx); + }); + } + + let mut lines = self .session .as_ref() .map(|s| s.get_lines()) - .unwrap_or_else(|| vec!["PTY unavailable".to_string()]); + .unwrap_or_else( + || vec![crate::i18n::menu_text(cx, "terminal.unavailable").to_string()], + ); + if self.session.is_some() && !self.pending.is_empty() { + lines.push(self.pending.clone()); + } v_flex() .size_full() // 终端区背景跟随主题(对齐 Tauri xterm 的 `--background`), // 浅色主题下不再残留深色底。 .bg(ThemeColors::background()) - .p_2() .child( - // 终端顶部控制栏 - h_flex() - .h(px(28.0)) - .w_full() - .justify_between() - .items_center() - .border_b_1() - .border_color(ThemeColors::border()) - .pb_1() - .child( - h_flex() - .items_center() - .gap_2() - .text_xs() - .text_color(ThemeColors::subtle_foreground()) - .child("Linux PTY Shell") - .child(format!("({})", self.working_dir)), - ) - .child( - Button::new("clear-term") - .small() - .ghost() - .label(crate::i18n::menu_text(cx, "ui.clear")) - .on_click(cx.listener(|this, _event, _window, cx| { - this.clear(cx); - })), - ), - ) - .child( - // 终端输出行展示区 + // 终端输出行展示区(控制栏由底部窗格头负责,不再自带)。 div() .flex_1() .w_full() + .min_h_0() .overflow_y_scrollbar() .p_2() .text_xs() @@ -236,10 +399,11 @@ impl Render for TerminalView { })), ) .child( - // 命令行输入条 + // 命令行输入条:可输入单行命令,回车或按钮发送。 h_flex() .h(px(32.0)) .w_full() + .flex_shrink_0() .bg(ThemeColors::surface()) .border_t_1() .border_color(ThemeColors::border()) @@ -256,24 +420,25 @@ impl Render for TerminalView { .child( div() .flex_1() - .text_xs() - .text_color(ThemeColors::foreground()) - .font_family("monospace") - .child(if self.current_input.is_empty() { - "Type command here...".to_string() - } else { - self.current_input.clone() - }), + .child(Input::new(&input_entity).cleanable(true)), ) .child( Button::new("run-cmd") .small() .primary() - .label("Execute") - .on_click(cx.listener(|this, _event, _window, cx| { - let cmd = this.current_input.clone(); - if !cmd.is_empty() { - this.send_command(&cmd, cx); + .label(crate::i18n::menu_text(cx, "terminal.execute")) + .on_click(cx.listener(move |_this, _event, window, cx| { + let cmd = input_entity.read(cx).value().to_string(); + if !cmd.trim().is_empty() { + let _ = _this.session.as_ref().map(|session| { + let mut full = cmd.clone(); + full.push('\n'); + let _ = session.write_input(&full); + }); + input_entity.update(cx, |state, cx| { + state.set_value("", window, cx); + }); + cx.notify(); } })), ), From 28ca208ede0ec4bed9fe266cd6c58a012975ee8a Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 01:39:54 +0800 Subject: [PATCH 46/59] =?UTF-8?q?feat(linux):=20=E7=BB=88=E7=AB=AF?= =?UTF-8?q?=E6=94=B9=20IDEA=20=E5=BC=8F=E5=AD=97=E7=AC=A6=E7=9B=B4?= =?UTF-8?q?=E8=BE=93=EF=BC=88vte=20=E7=9C=9F=E8=A7=A3=E6=9E=90=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 vte 依赖做转义解析:颜色/加粗/擦除/光标移动 - 点击聚焦后键盘直输 PTY:历史补全方向键中断均可用 - 聚焦显示反色光标;输出实时流式;中文直输 - 删底部输入条与自带控制栏;文案走 terminal.* 键 --- linux/Cargo.lock | 11 + linux/Cargo.toml | 1 + linux/src/i18n.rs | 1 - linux/src/workbench/terminal.rs | 869 +++++++++++++++++++++++--------- 4 files changed, 646 insertions(+), 236 deletions(-) diff --git a/linux/Cargo.lock b/linux/Cargo.lock index 600338172..fdd5114ed 100644 --- a/linux/Cargo.lock +++ b/linux/Cargo.lock @@ -3923,6 +3923,7 @@ dependencies = [ "serde_json", "tokio", "uuid", + "vte", ] [[package]] @@ -8098,6 +8099,16 @@ dependencies = [ "libc", ] +[[package]] +name = "vte" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd" +dependencies = [ + "arrayvec", + "memchr", +] + [[package]] name = "waker-fn" version = "1.2.0" diff --git a/linux/Cargo.toml b/linux/Cargo.toml index 497950b15..2f0975aa7 100644 --- a/linux/Cargo.toml +++ b/linux/Cargo.toml @@ -20,6 +20,7 @@ gpui-kit = { version = "0.6", features = [ ] } lithe-core = { path = "../rust/lithe-core" } portable-pty = "0.9" +vte = "0.15" serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["sync", "macros", "rt"] } diff --git a/linux/src/i18n.rs b/linux/src/i18n.rs index 086a2f227..1750e59b3 100644 --- a/linux/src/i18n.rs +++ b/linux/src/i18n.rs @@ -166,7 +166,6 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { ), "terminal.cleared" => ("终端已清空。", "Terminal cleared."), "terminal.unavailable" => ("PTY 不可用", "PTY unavailable"), - "terminal.execute" => ("执行", "Execute"), "workbench.settings" => ("设置", "Settings"), "workbench.sourceControl" => ("源代码管理", "Source Control"), "fileExplorer.searchFiles" => ("搜索文件", "Search files"), diff --git a/linux/src/workbench/terminal.rs b/linux/src/workbench/terminal.rs index 3f13c63e6..d431084da 100644 --- a/linux/src/workbench/terminal.rs +++ b/linux/src/workbench/terminal.rs @@ -1,97 +1,406 @@ -use gpui_kit::component::button::{Button, ButtonVariants as _}; -use gpui_kit::component::input::{Input, InputEvent, InputState}; +//! Linux 原生 PTY 终端:`portable-pty` 跑 shell,`vte`(Alacritty 解析器) +//! 做真机解析,键盘字符模式直输(对齐 IDEA 内嵌终端的交互:历史、 +//! 补全、方向键、中断均由 PTY 行规程处理)。 +//! +//! 显示模型为行式 + 样式段(非全网格):处理打印 / 换行 / 回车覆盖 / +//! 退格 / SGR 颜色 / 擦除 / 光标移动;输出按行累积(上限丢弃最旧)。 +//! 适配声明:多标签、新终端、终端内搜索、复制粘贴、全屏、滚到末尾跟随、 +//! PTY 随窗缩放暂不支持(缺 xterm 级组件与滚动控制,采固定 120x30)。 + use gpui_kit::component::scroll::ScrollableElement as _; -use gpui_kit::component::{h_flex, v_flex, Sizable as _}; +use gpui_kit::component::{h_flex, v_flex}; +use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::{ - div, px, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement, - ParentElement as _, Render, Styled as _, Subscription, Window, + div, px, Context, FocusHandle, FontWeight, InteractiveElement as _, IntoElement, KeyDownEvent, + ParentElement as _, Render, Rgba, StatefulInteractiveElement as _, Styled as _, Window, }; use portable_pty::{native_pty_system, CommandBuilder, PtySize}; use std::io::{Read, Write}; use std::sync::{mpsc, Arc, Mutex}; use std::thread; +use vte::{Parser, Perform}; use crate::settings; use crate::theme::ThemeColors; -/// 输出行数上限。 +/// PTY 固定尺寸(随窗缩放暂不支持)。 +const TERMINAL_COLS: usize = 120; +const TERMINAL_ROWS: u16 = 30; + +/// 输出行数上限,超出丢弃最旧。 const MAX_TERMINAL_LINES: usize = 1000; -/// 未换行缓冲上限(字符),超出保留末尾,避免无换物流撑爆内存。 -const MAX_PENDING_CHARS: usize = 8192; -const PENDING_KEEP_CHARS: usize = 4096; - -/// 剥离 ANSI 转义序列与无用控制字符(保留 `\n` `\r` `\t` 交由换行逻辑处理)。 -/// 无 xterm 网格,按纯文本展示是既定适配(右键菜单/复制等同理缺失)。 -fn strip_ansi(input: &str) -> String { - let mut out = String::with_capacity(input.len()); - let mut chars = input.chars().peekable(); - while let Some(ch) = chars.next() { - if ch == '\x1b' { - match chars.peek() { - // CSI:`ESC [` … 最终字节 `@`..=`~` - Some('[') => { - chars.next(); - loop { - match chars.next() { - None => break, - Some(c) if ('@'..='~').contains(&c) => break, - Some(_) => {} - } - } +/// 渲染行数上限(样式段已合并,超出只保留末尾)。 +const MAX_RENDER_LINES: usize = 600; + +/// xterm 基础 16 色(加粗 0-7 自动取 8-15,标准终端行为)。 +const ANSI_PALETTE: [(u8, u8, u8); 16] = [ + (0, 0, 0), + (205, 0, 0), + (0, 205, 0), + (205, 205, 0), + (0, 0, 238), + (205, 0, 205), + (0, 205, 205), + (229, 229, 229), + (127, 127, 127), + (255, 0, 0), + (0, 255, 0), + (255, 255, 0), + (92, 92, 255), + (255, 0, 255), + (0, 255, 255), + (255, 255, 255), +]; + +/// 256 色:0-15 基础色,16-231 立方体,232-255 灰阶。 +fn palette_256(index: u8) -> (u8, u8, u8) { + match index { + 0..=15 => ANSI_PALETTE[index as usize], + 16..=231 => { + let n = index - 16; + let levels = [0u8, 95, 135, 175, 215, 255]; + ( + levels[(n / 36) as usize], + levels[((n % 36) / 6) as usize], + levels[(n % 6) as usize], + ) + } + _ => { + let v = 8 + (index - 232) * 10; + (v, v, v) + } + } +} + +fn rgba_of(rgb: (u8, u8, u8)) -> Rgba { + Rgba { + r: rgb.0 as f32 / 255.0, + g: rgb.1 as f32 / 255.0, + b: rgb.2 as f32 / 255.0, + a: 1.0, + } +} + +/// 语义颜色:默认色渲染时取主题,索引/RGB 取调色板。 +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +enum TermColor { + #[default] + Default, + Indexed(u8), + Rgb(u8, u8, u8), +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +struct SpanStyle { + fg: TermColor, + bg: TermColor, + bold: bool, +} + +#[derive(Debug, Clone)] +struct StyledChar { + c: char, + style: SpanStyle, +} + +/// 终端屏幕:行缓冲(末尾即当前行)+ 光标 + 当前样式 + 保存位。 +struct TermScreen { + lines: Vec>, + row: usize, + col: usize, + style: SpanStyle, + saved: Option<(usize, usize)>, +} + +impl TermScreen { + fn new() -> Self { + Self { + lines: vec![Vec::new()], + row: 0, + col: 0, + style: SpanStyle::default(), + saved: None, + } + } + + fn clear_all(&mut self) { + self.lines = vec![Vec::new()]; + self.row = 0; + self.col = 0; + self.style = SpanStyle::default(); + } + + /// 保证 `row` 行存在,返回其可变引用(行号超限自动补空行)。 + fn row_mut(&mut self, row: usize) -> &mut Vec { + while self.lines.len() <= row { + self.lines.push(Vec::new()); + } + &mut self.lines[row] + } + + /// 换行:光标移到下一行行首,超限丢弃最旧行。 + fn newline(&mut self) { + self.row += 1; + self.col = 0; + self.row_mut(self.row); + if self.lines.len() > MAX_TERMINAL_LINES { + let overflow = self.lines.len() - MAX_TERMINAL_LINES; + self.lines.drain(..overflow); + self.row = self.row.saturating_sub(overflow); + } + } + + /// 在光标处打印字符(覆盖写,超宽自动换行)。 + fn put_char(&mut self, c: char) { + if self.col >= TERMINAL_COLS { + self.newline(); + } + let style = self.style; + let col = self.col; + let row = self.row; + let line = self.row_mut(row); + if col < line.len() { + line[col] = StyledChar { c, style }; + } else { + while line.len() < col { + line.push(StyledChar { + c: ' ', + style: SpanStyle::default(), + }); + } + line.push(StyledChar { c, style }); + } + self.col += 1; + } +} + +/// 取第 `index` 个参数(无参回退 `default`)。 +fn csi_param(params: &vte::Params, index: usize, default: u16) -> u16 { + params + .iter() + .flat_map(|group| group.iter().copied()) + .nth(index) + .unwrap_or(default) +} + +impl Perform for TermScreen { + fn print(&mut self, c: char) { + self.put_char(c); + } + + fn execute(&mut self, byte: u8) { + match byte { + b'\n' | b'\x0b' | b'\x0c' => self.newline(), + b'\r' => self.col = 0, + // 退格:只回退不删除(覆盖写模型)。 + 0x08 => self.col = self.col.saturating_sub(1), + // 制表:到下一个 8 列停靠位。 + b'\t' => self.col = (self.col + 8) & !7, + // BEL 等其余控制字符忽略。 + _ => {} + } + } + + fn csi_dispatch( + &mut self, + params: &vte::Params, + _intermediates: &[u8], + _ignore: bool, + action: char, + ) { + match action { + // 光标移动 + 'A' => self.row = self.row.saturating_sub(csi_param(params, 0, 1) as usize), + 'B' => { + let row = self.row + csi_param(params, 0, 1) as usize; + self.row = row; + self.row_mut(row); + } + 'C' => self.col += csi_param(params, 0, 1) as usize, + 'D' => self.col = self.col.saturating_sub(csi_param(params, 0, 1) as usize), + 'E' => { + self.row += csi_param(params, 0, 1) as usize; + self.col = 0; + let row = self.row; + self.row_mut(row); + } + 'F' => { + self.row = self.row.saturating_sub(csi_param(params, 0, 1) as usize); + self.col = 0; + } + 'G' => self.col = csi_param(params, 0, 1).saturating_sub(1) as usize, + 'H' | 'f' => { + self.row = csi_param(params, 0, 1).saturating_sub(1) as usize; + self.col = csi_param(params, 1, 1).saturating_sub(1) as usize; + let row = self.row; + self.row_mut(row); + } + // 擦除显示 + 'J' => match csi_param(params, 0, 0) { + 1 => { + self.lines.drain(..self.row.min(self.lines.len())); + self.row = 0; + let row = self.row; + self.row_mut(row).clear(); } - // OSC:`ESC ]` … 以 BEL 或 `ESC \` 结束 - Some(']') => { - chars.next(); - let mut prev_esc = false; - loop { - match chars.next() { - None => break, - Some('\x07') => break, - Some('\x1b') => prev_esc = true, - Some('\\') if prev_esc => break, - Some(_) => prev_esc = false, + 2 | 3 => self.clear_all(), + // 0:光标到末尾清掉 + _ => { + let row = self.row; + let col = self.col; + let line = self.row_mut(row); + line.truncate(col.min(line.len())); + self.lines.truncate(row + 1); + } + }, + // 擦除行 + 'K' => match csi_param(params, 0, 0) { + 1 => { + let row = self.row; + let col = self.col; + let line = self.row_mut(row); + line.drain(..col.min(line.len())); + self.col = 0; + } + 2 => { + let row = self.row; + self.row_mut(row).clear(); + self.col = 0; + } + // 0:光标到行尾清掉 + _ => { + let row = self.row; + let col = self.col; + let len = self.row_mut(row).len(); + self.row_mut(row).truncate(col.min(len)); + } + }, + // 上卷:丢弃顶部 n 行 + 'S' => { + let n = csi_param(params, 0, 1) as usize; + let drop = n.min(self.lines.len().saturating_sub(1)); + self.lines.drain(..drop); + self.row = self.row.saturating_sub(drop); + } + // 光标存取 + 's' => self.saved = Some((self.row, self.col)), + 'u' => { + if let Some((row, col)) = self.saved { + self.row = row; + self.col = col; + self.row_mut(row); + } + } + // SGR 颜色与属性 + 'm' => { + let values: Vec = params.iter().flat_map(|g| g.iter().copied()).collect(); + let values = if values.is_empty() { vec![0] } else { values }; + let mut ix = 0; + while ix < values.len() { + match values[ix] { + 0 => self.style = SpanStyle::default(), + 1 => self.style.bold = true, + 22 => self.style.bold = false, + 30..=37 => { + self.style.fg = TermColor::Indexed((values[ix] - 30) as u8); } + 39 => self.style.fg = TermColor::Default, + 40..=47 => { + self.style.bg = TermColor::Indexed((values[ix] - 40) as u8); + } + 49 => self.style.bg = TermColor::Default, + 90..=97 => { + self.style.fg = TermColor::Indexed((values[ix] - 90 + 8) as u8); + } + 100..=107 => { + self.style.bg = TermColor::Indexed((values[ix] - 100 + 8) as u8); + } + 38 | 48 => { + let is_fg = values[ix] == 38; + let color = match values.get(ix + 1).copied() { + Some(5) => values + .get(ix + 2) + .copied() + .map(|v| TermColor::Indexed(v.min(255) as u8)), + Some(2) => match ( + values.get(ix + 2).copied(), + values.get(ix + 3).copied(), + values.get(ix + 4).copied(), + ) { + (Some(r), Some(g), Some(b)) => Some(TermColor::Rgb( + r.min(255) as u8, + g.min(255) as u8, + b.min(255) as u8, + )), + _ => None, + }, + _ => None, + }; + if let Some(color) = color { + if is_fg { + self.style.fg = color; + } else { + self.style.bg = color; + } + } + ix += 4; + } + _ => {} } + ix += 1; } - // 字符集选择等两字符序列 - Some('(') | Some(')') | Some('#') => { - chars.next(); - chars.next(); + } + _ => {} + } + } + + fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, byte: u8) { + match byte { + // RI:反向换行 + b'M' => { + if self.row == 0 { + self.lines.insert(0, Vec::new()); + if self.lines.len() > MAX_TERMINAL_LINES { + self.lines.pop(); + } + } else { + self.row -= 1; } - Some(_) => { - chars.next(); + } + // DECSC / DECRC:光标存取 + b'7' => self.saved = Some((self.row, self.col)), + b'8' => { + if let Some((row, col)) = self.saved { + self.row = row; + self.col = col; + self.row_mut(row); } - None => {} } - } else if ch.is_control() && ch != '\n' && ch != '\r' && ch != '\t' { - // 丢弃 BEL 等无用控制字符 - } else { - out.push(ch); + // RIS:全复位 + b'c' => self.clear_all(), + _ => {} } } - out } /// Linux 原生 PTY 会话 pub struct TerminalSession { writer: Arc>>, - pub output_lines: Arc>>, + /// 主端句柄:必须活着,丢弃即关闭 PTY(shell 收 SIGHUP 退出)。 #[allow(dead_code)] - input_buffer: Arc>, + master: Arc>>, } impl TerminalSession { - /// 打开 PTY 会话并起输出线程:线程只透传原始字节块,主线程泵负责 - /// 解析换行/回车并 `notify`(输出实时出现,对齐 xterm 的流式展示)。 - /// 返回会话与输出 channel;`banner` 为首行提示(调用方按语言构造)。 + /// 打开 PTY 会话并起输出线程:线程只透传原始字节块,主线程泵喂给 + /// `vte` 解析器(输出实时出现,对齐 xterm 的流式展示)。 pub fn new( cols: u16, rows: u16, working_dir: &str, shell: &str, - banner: Option, - ) -> anyhow::Result<(Self, mpsc::Receiver)> { + ) -> anyhow::Result<(Self, mpsc::Receiver>, String)> { let pty_system = native_pty_system(); let pair = pty_system.openpty(PtySize { rows, @@ -111,20 +420,16 @@ impl TerminalSession { let _child = pair.slave.spawn_command(cmd)?; let mut reader = pair.master.try_clone_reader()?; let writer = pair.master.take_writer()?; + let displayed_shell = shell.clone(); - let output_lines = Arc::new(Mutex::new( - banner.map(|line| vec![line]).unwrap_or_default(), - )); - let (tx, rx) = mpsc::channel::(); - + let (tx, rx) = mpsc::channel::>(); thread::spawn(move || { let mut buf = [0u8; 4096]; loop { match reader.read(&mut buf) { Ok(0) | Err(_) => break, Ok(n) => { - let chunk = String::from_utf8_lossy(&buf[..n]).into_owned(); - if tx.send(chunk).is_err() { + if tx.send(buf[..n].to_vec()).is_err() { break; } } @@ -135,35 +440,45 @@ impl TerminalSession { Ok(( Self { writer: Arc::new(Mutex::new(writer)), - output_lines, - input_buffer: Arc::new(Mutex::new(String::new())), + master: Arc::new(Mutex::new(pair.master)), }, rx, + displayed_shell, )) } pub fn write_input(&self, input: &str) -> anyhow::Result<()> { + self.write_bytes(input.as_bytes()) + } + + pub fn write_bytes(&self, bytes: &[u8]) -> anyhow::Result<()> { let mut writer = self.writer.lock().unwrap(); - writer.write_all(input.as_bytes())?; + writer.write_all(bytes)?; writer.flush()?; Ok(()) } - pub fn get_lines(&self) -> Vec { - self.output_lines.lock().unwrap().clone() + /// PTY 缩放(随窗缩放接入后调用,当前固定尺寸保留接口)。 + #[allow(dead_code)] + pub fn resize(&self, cols: u16, rows: u16) { + if let Ok(master) = self.master.lock() { + let _ = master.resize(PtySize { + rows, + cols, + pixel_width: 0, + pixel_height: 0, + }); + } } } -/// 终端视图组件:输出区 + 可输入的命令行(回车/按钮发送整行)。 +/// 终端视图组件:点击聚焦后键盘字符模式直输(IDEA 式交互)。 pub struct TerminalView { pub session: Option, pub working_dir: String, - /// 未换行缓冲:跨字节块累积,`\r` 覆盖当前行内容。 - pending: String, - input: Option>, - _input_subscription: Option, - /// 回车发送后待清空输入框(render 内有 `window` 才可执行)。 - clear_pending: bool, + screen: TermScreen, + parser: Parser, + focus_handle: FocusHandle, } impl TerminalView { @@ -171,29 +486,32 @@ impl TerminalView { let mut view = Self { session: None, working_dir, - pending: String::new(), - input: None, - _input_subscription: None, - clear_pending: false, + screen: TermScreen::new(), + parser: Parser::new(), + focus_handle: cx.focus_handle(), }; view.attach_session(cx); view } - /// 打开 PTY 会话并起主线程输出泵(channel 收字节块 → 解析落行 → notify)。 + /// 打开 PTY 会话并起主线程输出泵(channel 收字节块 → `vte` 解析 → notify)。 fn attach_session(&mut self, cx: &mut Context) { - let banner = crate::i18n::menu_text(cx, "terminal.session") - .replace("{shell}", &Self::resolve_shell(cx)); + let shell = Self::resolve_shell(cx); match TerminalSession::new( - 120, - 30, + TERMINAL_COLS as u16, + TERMINAL_ROWS, &self.working_dir, - &Self::resolve_shell(cx), - Some(banner), + &shell, ) { - Ok((session, rx)) => { + Ok((session, rx, displayed)) => { self.session = Some(session); - self.pending.clear(); + self.screen.clear_all(); + let banner = + crate::i18n::menu_text(cx, "terminal.session").replace("{shell}", &displayed); + for c in banner.chars() { + self.screen.put_char(c); + } + self.screen.newline(); Self::spawn_pump(rx, cx); } Err(_) => { @@ -203,8 +521,8 @@ impl TerminalView { cx.notify(); } - /// 主线程输出泵:后台收字节块,回主线程解析。 - fn spawn_pump(rx: mpsc::Receiver, cx: &mut Context) { + /// 主线程输出泵:后台收字节块,回主线程喂解析器。 + fn spawn_pump(rx: mpsc::Receiver>, cx: &mut Context) { let rx = Arc::new(Mutex::new(rx)); cx.spawn(async move |this, cx| loop { let slot = rx.clone(); @@ -216,10 +534,15 @@ impl TerminalView { }) .await; match next { - Some(chunk) => { + Some(bytes) => { if this .update(cx, |view, cx| { - view.push_chunk(&chunk); + view.parser.advance(&mut view.screen, &bytes); + if view.screen.lines.len() > MAX_TERMINAL_LINES { + let overflow = view.screen.lines.len() - MAX_TERMINAL_LINES; + view.screen.lines.drain(..overflow); + view.screen.row = view.screen.row.saturating_sub(overflow); + } cx.notify(); }) .is_err() @@ -233,37 +556,6 @@ impl TerminalView { .detach(); } - /// 解析输出块:ANSI 剥离后按换行落行、`\r` 覆盖当前行。 - fn push_chunk(&mut self, chunk: &str) { - let text = strip_ansi(chunk); - let Some(session) = self.session.as_ref() else { - return; - }; - let Ok(mut lines) = session.output_lines.lock() else { - return; - }; - for piece in text.split_inclusive('\n') { - let (content, newline) = match piece.strip_suffix('\n') { - Some(content) => (content, true), - None => (piece, false), - }; - // `\r` 语义为回车覆盖:只保留最后一个 `\r` 之后的内容。 - let visible = content.rsplit('\r').next().unwrap_or(""); - self.pending.push_str(visible); - let count = self.pending.chars().count(); - if count > MAX_PENDING_CHARS { - let skip = count - PENDING_KEEP_CHARS; - self.pending = self.pending.chars().skip(skip).collect(); - } - if newline { - lines.push(std::mem::take(&mut self.pending)); - if lines.len() > MAX_TERMINAL_LINES { - lines.remove(0); - } - } - } - } - /// 解析生效 shell:设置 `terminalDefaultShellId` 非空即用(名称经 /// `PATH` 或 `/bin` 解析为路径),为空回退 `$SHELL`。 fn resolve_shell(cx: &gpui_kit::App) -> String { @@ -292,6 +584,7 @@ impl TerminalView { id } + /// 程序化发送命令(宿主调用,如打开目录):原文 + 换行。 pub fn send_command(&mut self, cmd: &str, cx: &mut Context) { if let Some(session) = &self.session { let mut full = cmd.to_string(); @@ -302,13 +595,10 @@ impl TerminalView { } pub fn clear(&mut self, cx: &mut Context) { - if let Some(session) = &self.session { - if let Ok(mut lines) = session.output_lines.lock() { - lines.clear(); - lines.push(crate::i18n::menu_text(cx, "terminal.cleared").to_string()); - } - } - self.pending.clear(); + self.screen.clear_all(); + self.screen + .put_str(&crate::i18n::menu_text(cx, "terminal.cleared").to_string()); + self.screen.newline(); cx.notify(); } @@ -318,68 +608,116 @@ impl TerminalView { self.attach_session(cx); } - /// 懒创建命令行输入框。 - fn ensure_input( - slot: &mut Option>, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - if let Some(entity) = slot.clone() { - return entity; + /// 键盘直输:可打印字符原样写 PTY,控制组合与功能键转义序列, + /// 未知键放行(IDEA 快捷键不断)。返回是否消费。 + fn handle_key(&mut self, event: &KeyDownEvent) -> bool { + let key = event.keystroke.key.as_str(); + let mods = &event.keystroke.modifiers; + let bytes: Vec = if mods.control && !mods.alt && !mods.platform { + // Ctrl+字母 → 控制字符(Ctrl+C 中断等)。 + let lower = key.to_lowercase(); + let mut chars = lower.chars(); + match (chars.next(), chars.next()) { + (Some(c @ 'a'..='z'), None) => vec![(c as u8) - b'a' + 1], + (Some(' '), None) => vec![0], + (Some('['), None) => vec![0x1b], + (Some('\\'), None) => vec![0x1c], + (Some(']'), None) => vec![0x1d], + (Some('^'), None) => vec![0x1e], + (Some('_'), None) => vec![0x1f], + _ => return false, + } + } else if mods.alt && !mods.control && !mods.platform { + // Alt+字符 → ESC 前缀(readline Meta 键)。 + match &event.keystroke.key_char { + Some(text) if text.chars().count() == 1 => { + let mut out = vec![0x1b]; + out.extend_from_slice(text.as_bytes()); + out + } + _ => return false, + } + } else if mods.control || mods.platform { + return false; + } else { + match key { + "enter" => b"\r".to_vec(), + "backspace" => vec![0x7f], + "tab" => b"\t".to_vec(), + "escape" => vec![0x1b], + "up" => b"\x1b[A".to_vec(), + "down" => b"\x1b[B".to_vec(), + "right" => b"\x1b[C".to_vec(), + "left" => b"\x1b[D".to_vec(), + "home" => b"\x1b[H".to_vec(), + "end" => b"\x1b[F".to_vec(), + "delete" => b"\x1b[3~".to_vec(), + "pageup" => b"\x1b[5~".to_vec(), + "pagedown" => b"\x1b[6~".to_vec(), + "f1" => b"\x1bOP".to_vec(), + "f2" => b"\x1bOQ".to_vec(), + "f3" => b"\x1bOR".to_vec(), + "f4" => b"\x1bOS".to_vec(), + "f5" => b"\x1b[15~".to_vec(), + "f6" => b"\x1b[17~".to_vec(), + "f7" => b"\x1b[18~".to_vec(), + "f8" => b"\x1b[19~".to_vec(), + "f9" => b"\x1b[20~".to_vec(), + "f10" => b"\x1b[21~".to_vec(), + "f11" => b"\x1b[23~".to_vec(), + "f12" => b"\x1b[24~".to_vec(), + _ => match &event.keystroke.key_char { + // 可打印字符(含中文)原样直输。 + Some(text) => text.as_bytes().to_vec(), + None => return false, + }, + } + }; + if let Some(session) = &self.session { + let _ = session.write_bytes(&bytes); } - let entity = cx.new(|cx| InputState::new(window, cx)); - *slot = Some(entity.clone()); - entity + true } } -impl Render for TerminalView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - // 命令行输入框懒创建;回车发送整行(`PressEnter`),发送后在本次 - // render 内清空(`set_value` 需 `window`,订阅回调里拿不到)。 - let input_entity = Self::ensure_input(&mut self.input, window, cx); - if self._input_subscription.is_none() { - let entity = input_entity.clone(); - self._input_subscription = Some(cx.subscribe( - &input_entity, - move |this: &mut Self, _, event: &InputEvent, cx| { - if matches!(event, InputEvent::PressEnter { .. }) { - let cmd = entity.read(cx).value().to_string(); - if !cmd.trim().is_empty() { - this.send_command(&cmd, cx); - this.clear_pending = true; - cx.notify(); - } - } - }, - )); - } - if self.clear_pending { - self.clear_pending = false; - let entity = input_entity.clone(); - entity.update(cx, |state, cx| { - state.set_value("", window, cx); - }); +impl TermScreen { + /// 纯文本写入(banner/清空提示用当前默认样式)。 + fn put_str(&mut self, text: &str) { + for c in text.chars() { + if c == '\n' { + self.newline(); + } else { + self.put_char(c); + } } + } +} - let mut lines = self - .session - .as_ref() - .map(|s| s.get_lines()) - .unwrap_or_else( - || vec![crate::i18n::menu_text(cx, "terminal.unavailable").to_string()], - ); - if self.session.is_some() && !self.pending.is_empty() { - lines.push(self.pending.clone()); - } +impl Render for TerminalView { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let focused = self.focus_handle.is_focused(window); + let total = self.screen.lines.len(); + let start = total.saturating_sub(MAX_RENDER_LINES); + let default_fg = ThemeColors::foreground(); + let default_bg = ThemeColors::background(); v_flex() .size_full() - // 终端区背景跟随主题(对齐 Tauri xterm 的 `--background`), - // 浅色主题下不再残留深色底。 .bg(ThemeColors::background()) + .id("terminal-grid") + .track_focus(&self.focus_handle) + .cursor_text() + .on_click(cx.listener(|this, _event, window, cx| { + window.focus(&this.focus_handle, cx); + })) + .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { + if this.handle_key(event) { + cx.stop_propagation(); + cx.notify(); + } + })) .child( - // 终端输出行展示区(控制栏由底部窗格头负责,不再自带)。 + // 输出网格(控制栏由底部窗格头负责,不再自带)。 div() .flex_1() .w_full() @@ -388,60 +726,121 @@ impl Render for TerminalView { .p_2() .text_xs() .text_size(px(crate::settings::get(cx).terminal_font_size)) - .text_color(ThemeColors::foreground()) .font_family("monospace") - .children(lines.into_iter().enumerate().map(|(idx, line)| { - div().id(idx).child(if line.is_empty() { - " ".to_string() - } else { - line - }) - })), - ) - .child( - // 命令行输入条:可输入单行命令,回车或按钮发送。 - h_flex() - .h(px(32.0)) - .w_full() - .flex_shrink_0() - .bg(ThemeColors::surface()) - .border_t_1() - .border_color(ThemeColors::border()) - .items_center() - .px_2() - .gap_2() - .child( - div() - .text_xs() - .text_color(ThemeColors::success()) - .font_family("monospace") - .child("$"), - ) - .child( - div() - .flex_1() - .child(Input::new(&input_entity).cleanable(true)), - ) - .child( - Button::new("run-cmd") - .small() - .primary() - .label(crate::i18n::menu_text(cx, "terminal.execute")) - .on_click(cx.listener(move |_this, _event, window, cx| { - let cmd = input_entity.read(cx).value().to_string(); - if !cmd.trim().is_empty() { - let _ = _this.session.as_ref().map(|session| { - let mut full = cmd.clone(); - full.push('\n'); - let _ = session.write_input(&full); - }); - input_entity.update(cx, |state, cx| { - state.set_value("", window, cx); - }); - cx.notify(); - } - })), + .children( + self.screen.lines[start..] + .iter() + .enumerate() + .map(|(ix, line)| { + render_term_line( + start + ix, + line, + focused && start + ix == self.screen.row, + self.screen.col, + default_fg, + default_bg, + ) + }) + .collect::>(), ), ) } } + +/// 语义颜色解算(加粗配 0-7 自动取高亮 variant,标准终端行为)。 +fn resolve_color(color: TermColor, bold: bool, default: Rgba) -> Rgba { + match color { + TermColor::Default => default, + TermColor::Indexed(i) => { + let i = if bold && i < 8 { i + 8 } else { i }; + rgba_of(palette_256(i)) + } + TermColor::Rgb(r, g, b) => rgba_of((r, g, b)), + } +} + +/// 渲染一行:同样式字符合并为一段;聚焦时在光标处画反色块。 +fn render_term_line( + row: usize, + line: &[StyledChar], + cursor_here: bool, + cursor_col: usize, + default_fg: Rgba, + default_bg: Rgba, +) -> gpui_kit::AnyElement { + // 合并同样式段。 + let mut spans: Vec<(SpanStyle, String)> = Vec::new(); + for ch in line { + if let Some((style, text)) = spans.last_mut() { + if *style == ch.style { + text.push(ch.c); + continue; + } + } + spans.push((ch.style, ch.c.to_string())); + } + let total: usize = spans.iter().map(|(_, text)| text.chars().count()).sum(); + // 光标超出文本末尾时在行尾补空格块。 + let trailing = cursor_here && cursor_col >= total; + let mut row_el = h_flex() + .id(row) + .w_full() + .items_center() + .text_xs() + .font_family("monospace"); + if spans.is_empty() && !trailing { + return row_el + .child(div().child(" ".to_string())) + .into_any_element(); + } + let mut col = 0; + for (style, text) in &spans { + let fg = resolve_color(style.fg, style.bold, default_fg); + let bg = resolve_color(style.bg, false, default_bg); + let chunk = |text: String| { + div() + .text_color(fg) + .when(style.bg != TermColor::Default, |el| el.bg(bg)) + .when(style.bold, |el| el.font_weight(FontWeight::BOLD)) + .child(text) + }; + if trailing { + row_el = row_el.child(chunk(text.clone())); + continue; + } + let mut before = String::new(); + let mut cursor_char: Option = None; + let mut after = String::new(); + for c in text.chars() { + if col < cursor_col { + before.push(c); + } else if cursor_char.is_none() { + cursor_char = Some(c); + } else { + after.push(c); + } + col += 1; + } + if !before.is_empty() { + row_el = row_el.child(chunk(before)); + } + row_el = row_el.child( + div() + .text_color(default_bg) + .bg(fg) + .child(cursor_char.unwrap_or(' ').to_string()), + ); + if !after.is_empty() { + row_el = row_el.child(chunk(after)); + } + } + if trailing { + row_el = row_el.child( + div() + .text_color(default_bg) + .bg(default_fg) + .child(" ".to_string()), + ); + } + row_el.into_any_element() +} From 64a90ad4465ba2fe10439b65206db76e8d9d8649 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 11:17:20 +0800 Subject: [PATCH 47/59] =?UTF-8?q?feat(linux):=20=E7=BB=88=E7=AB=AF?= =?UTF-8?q?=E6=8D=A2=20alacritty=5Fterminal=20=E7=BD=91=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 行式自研解析换 Alacritty 网格状态机(颜色/光标/擦除/滚屏全真) - 渲染可见区网格,同样式合并;聚焦反色光标块 - 应用光标键模式;清屏走 2J;删 vte 直依赖 --- linux/Cargo.lock | 79 ++++- linux/Cargo.toml | 2 +- linux/src/workbench/terminal.rs | 575 ++++++++++---------------------- 3 files changed, 249 insertions(+), 407 deletions(-) diff --git a/linux/Cargo.lock b/linux/Cargo.lock index fdd5114ed..0b98ccd21 100644 --- a/linux/Cargo.lock +++ b/linux/Cargo.lock @@ -131,6 +131,31 @@ 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" @@ -664,6 +689,9 @@ 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" @@ -1417,6 +1445,12 @@ dependencies = [ "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" @@ -3179,6 +3213,15 @@ 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" @@ -3914,6 +3957,7 @@ dependencies = [ name = "lithe-linux" version = "0.1.0" dependencies = [ + "alacritty_terminal", "anyhow", "gpui-kit", "lithe-core", @@ -3923,7 +3967,6 @@ dependencies = [ "serde_json", "tokio", "uuid", - "vte", ] [[package]] @@ -4235,6 +4278,15 @@ dependencies = [ "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" @@ -6248,6 +6300,17 @@ dependencies = [ "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" @@ -6699,6 +6762,16 @@ 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" @@ -8106,7 +8179,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd" dependencies = [ "arrayvec", + "bitflags 2.13.2", + "cursor-icon", + "log", "memchr", + "serde", ] [[package]] diff --git a/linux/Cargo.toml b/linux/Cargo.toml index 2f0975aa7..d14c4ac61 100644 --- a/linux/Cargo.toml +++ b/linux/Cargo.toml @@ -20,7 +20,7 @@ gpui-kit = { version = "0.6", features = [ ] } lithe-core = { path = "../rust/lithe-core" } portable-pty = "0.9" -vte = "0.15" +alacritty_terminal = "0.26" serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["sync", "macros", "rt"] } diff --git a/linux/src/workbench/terminal.rs b/linux/src/workbench/terminal.rs index d431084da..48163bc62 100644 --- a/linux/src/workbench/terminal.rs +++ b/linux/src/workbench/terminal.rs @@ -1,12 +1,16 @@ -//! Linux 原生 PTY 终端:`portable-pty` 跑 shell,`vte`(Alacritty 解析器) -//! 做真机解析,键盘字符模式直输(对齐 IDEA 内嵌终端的交互:历史、 -//! 补全、方向键、中断均由 PTY 行规程处理)。 +//! Linux 原生 PTY 终端:`portable-pty` 跑 shell,`alacritty_terminal` +//!(Alacritty 网格)做真机解析与状态,键盘字符模式直输(对齐 IDEA +//! 内嵌终端的交互:历史、补全、方向键、中断均由 PTY 行规程处理)。 //! -//! 显示模型为行式 + 样式段(非全网格):处理打印 / 换行 / 回车覆盖 / -//! 退格 / SGR 颜色 / 擦除 / 光标移动;输出按行累积(上限丢弃最旧)。 -//! 适配声明:多标签、新终端、终端内搜索、复制粘贴、全屏、滚到末尾跟随、 -//! PTY 随窗缩放暂不支持(缺 xterm 级组件与滚动控制,采固定 120x30)。 - +//! 渲染取网格可见区,同样式字符合并展示;聚焦时在光标处画反色块。 +//! 适配声明:多标签、新终端、终端内搜索、复制粘贴、滚到末尾跟随、 +//! PTY 随窗缩放暂不支持(缺选择/滚动控制,采固定 120x30 跟随显示)。 + +use alacritty_terminal::event::VoidListener; +use alacritty_terminal::grid::Dimensions; +use alacritty_terminal::term::cell::Flags; +use alacritty_terminal::term::{Config, Term, TermMode}; +use alacritty_terminal::vte::ansi::{Color as AlacColor, NamedColor, Processor, Rgb}; use gpui_kit::component::scroll::ScrollableElement as _; use gpui_kit::component::{h_flex, v_flex}; use gpui_kit::prelude::FluentBuilder as _; @@ -18,22 +22,35 @@ use portable_pty::{native_pty_system, CommandBuilder, PtySize}; use std::io::{Read, Write}; use std::sync::{mpsc, Arc, Mutex}; use std::thread; -use vte::{Parser, Perform}; use crate::settings; use crate::theme::ThemeColors; /// PTY 固定尺寸(随窗缩放暂不支持)。 const TERMINAL_COLS: usize = 120; -const TERMINAL_ROWS: u16 = 30; +const TERMINAL_ROWS: usize = 30; -/// 输出行数上限,超出丢弃最旧。 -const MAX_TERMINAL_LINES: usize = 1000; +/// 网格尺寸(`Term::new` 与 `resize` 共用)。 +struct TermDims { + cols: usize, + rows: usize, +} -/// 渲染行数上限(样式段已合并,超出只保留末尾)。 -const MAX_RENDER_LINES: usize = 600; +impl Dimensions for TermDims { + fn total_lines(&self) -> usize { + self.rows + } -/// xterm 基础 16 色(加粗 0-7 自动取 8-15,标准终端行为)。 + fn screen_lines(&self) -> usize { + self.rows + } + + fn columns(&self) -> usize { + self.cols + } +} + +/// xterm 基础 16 色。 const ANSI_PALETTE: [(u8, u8, u8); 16] = [ (0, 0, 0), (205, 0, 0), @@ -82,305 +99,23 @@ fn rgba_of(rgb: (u8, u8, u8)) -> Rgba { } } -/// 语义颜色:默认色渲染时取主题,索引/RGB 取调色板。 -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -enum TermColor { - #[default] - Default, - Indexed(u8), - Rgb(u8, u8, u8), -} - -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -struct SpanStyle { - fg: TermColor, - bg: TermColor, - bold: bool, -} - -#[derive(Debug, Clone)] -struct StyledChar { - c: char, - style: SpanStyle, -} - -/// 终端屏幕:行缓冲(末尾即当前行)+ 光标 + 当前样式 + 保存位。 -struct TermScreen { - lines: Vec>, - row: usize, - col: usize, - style: SpanStyle, - saved: Option<(usize, usize)>, -} - -impl TermScreen { - fn new() -> Self { - Self { - lines: vec![Vec::new()], - row: 0, - col: 0, - style: SpanStyle::default(), - saved: None, - } - } - - fn clear_all(&mut self) { - self.lines = vec![Vec::new()]; - self.row = 0; - self.col = 0; - self.style = SpanStyle::default(); - } - - /// 保证 `row` 行存在,返回其可变引用(行号超限自动补空行)。 - fn row_mut(&mut self, row: usize) -> &mut Vec { - while self.lines.len() <= row { - self.lines.push(Vec::new()); - } - &mut self.lines[row] - } - - /// 换行:光标移到下一行行首,超限丢弃最旧行。 - fn newline(&mut self) { - self.row += 1; - self.col = 0; - self.row_mut(self.row); - if self.lines.len() > MAX_TERMINAL_LINES { - let overflow = self.lines.len() - MAX_TERMINAL_LINES; - self.lines.drain(..overflow); - self.row = self.row.saturating_sub(overflow); - } - } - - /// 在光标处打印字符(覆盖写,超宽自动换行)。 - fn put_char(&mut self, c: char) { - if self.col >= TERMINAL_COLS { - self.newline(); - } - let style = self.style; - let col = self.col; - let row = self.row; - let line = self.row_mut(row); - if col < line.len() { - line[col] = StyledChar { c, style }; - } else { - while line.len() < col { - line.push(StyledChar { - c: ' ', - style: SpanStyle::default(), - }); - } - line.push(StyledChar { c, style }); - } - self.col += 1; - } -} - -/// 取第 `index` 个参数(无参回退 `default`)。 -fn csi_param(params: &vte::Params, index: usize, default: u16) -> u16 { - params - .iter() - .flat_map(|group| group.iter().copied()) - .nth(index) - .unwrap_or(default) -} - -impl Perform for TermScreen { - fn print(&mut self, c: char) { - self.put_char(c); - } - - fn execute(&mut self, byte: u8) { - match byte { - b'\n' | b'\x0b' | b'\x0c' => self.newline(), - b'\r' => self.col = 0, - // 退格:只回退不删除(覆盖写模型)。 - 0x08 => self.col = self.col.saturating_sub(1), - // 制表:到下一个 8 列停靠位。 - b'\t' => self.col = (self.col + 8) & !7, - // BEL 等其余控制字符忽略。 - _ => {} - } - } - - fn csi_dispatch( - &mut self, - params: &vte::Params, - _intermediates: &[u8], - _ignore: bool, - action: char, - ) { - match action { - // 光标移动 - 'A' => self.row = self.row.saturating_sub(csi_param(params, 0, 1) as usize), - 'B' => { - let row = self.row + csi_param(params, 0, 1) as usize; - self.row = row; - self.row_mut(row); - } - 'C' => self.col += csi_param(params, 0, 1) as usize, - 'D' => self.col = self.col.saturating_sub(csi_param(params, 0, 1) as usize), - 'E' => { - self.row += csi_param(params, 0, 1) as usize; - self.col = 0; - let row = self.row; - self.row_mut(row); - } - 'F' => { - self.row = self.row.saturating_sub(csi_param(params, 0, 1) as usize); - self.col = 0; - } - 'G' => self.col = csi_param(params, 0, 1).saturating_sub(1) as usize, - 'H' | 'f' => { - self.row = csi_param(params, 0, 1).saturating_sub(1) as usize; - self.col = csi_param(params, 1, 1).saturating_sub(1) as usize; - let row = self.row; - self.row_mut(row); - } - // 擦除显示 - 'J' => match csi_param(params, 0, 0) { - 1 => { - self.lines.drain(..self.row.min(self.lines.len())); - self.row = 0; - let row = self.row; - self.row_mut(row).clear(); - } - 2 | 3 => self.clear_all(), - // 0:光标到末尾清掉 - _ => { - let row = self.row; - let col = self.col; - let line = self.row_mut(row); - line.truncate(col.min(line.len())); - self.lines.truncate(row + 1); - } - }, - // 擦除行 - 'K' => match csi_param(params, 0, 0) { - 1 => { - let row = self.row; - let col = self.col; - let line = self.row_mut(row); - line.drain(..col.min(line.len())); - self.col = 0; - } - 2 => { - let row = self.row; - self.row_mut(row).clear(); - self.col = 0; - } - // 0:光标到行尾清掉 - _ => { - let row = self.row; - let col = self.col; - let len = self.row_mut(row).len(); - self.row_mut(row).truncate(col.min(len)); - } - }, - // 上卷:丢弃顶部 n 行 - 'S' => { - let n = csi_param(params, 0, 1) as usize; - let drop = n.min(self.lines.len().saturating_sub(1)); - self.lines.drain(..drop); - self.row = self.row.saturating_sub(drop); - } - // 光标存取 - 's' => self.saved = Some((self.row, self.col)), - 'u' => { - if let Some((row, col)) = self.saved { - self.row = row; - self.col = col; - self.row_mut(row); - } - } - // SGR 颜色与属性 - 'm' => { - let values: Vec = params.iter().flat_map(|g| g.iter().copied()).collect(); - let values = if values.is_empty() { vec![0] } else { values }; - let mut ix = 0; - while ix < values.len() { - match values[ix] { - 0 => self.style = SpanStyle::default(), - 1 => self.style.bold = true, - 22 => self.style.bold = false, - 30..=37 => { - self.style.fg = TermColor::Indexed((values[ix] - 30) as u8); - } - 39 => self.style.fg = TermColor::Default, - 40..=47 => { - self.style.bg = TermColor::Indexed((values[ix] - 40) as u8); - } - 49 => self.style.bg = TermColor::Default, - 90..=97 => { - self.style.fg = TermColor::Indexed((values[ix] - 90 + 8) as u8); - } - 100..=107 => { - self.style.bg = TermColor::Indexed((values[ix] - 100 + 8) as u8); - } - 38 | 48 => { - let is_fg = values[ix] == 38; - let color = match values.get(ix + 1).copied() { - Some(5) => values - .get(ix + 2) - .copied() - .map(|v| TermColor::Indexed(v.min(255) as u8)), - Some(2) => match ( - values.get(ix + 2).copied(), - values.get(ix + 3).copied(), - values.get(ix + 4).copied(), - ) { - (Some(r), Some(g), Some(b)) => Some(TermColor::Rgb( - r.min(255) as u8, - g.min(255) as u8, - b.min(255) as u8, - )), - _ => None, - }, - _ => None, - }; - if let Some(color) = color { - if is_fg { - self.style.fg = color; - } else { - self.style.bg = color; - } - } - ix += 4; - } - _ => {} - } - ix += 1; - } - } - _ => {} - } - } - - fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, byte: u8) { - match byte { - // RI:反向换行 - b'M' => { - if self.row == 0 { - self.lines.insert(0, Vec::new()); - if self.lines.len() > MAX_TERMINAL_LINES { - self.lines.pop(); - } - } else { - self.row -= 1; - } - } - // DECSC / DECRC:光标存取 - b'7' => self.saved = Some((self.row, self.col)), - b'8' => { - if let Some((row, col)) = self.saved { - self.row = row; - self.col = col; - self.row_mut(row); - } +/// 语义颜色解算:`Foreground`/`Background` 取主题,其余取调色板; +/// 加粗配 0-7 自动取高亮 variant(标准终端行为)。 +fn resolve_color(color: AlacColor, bold: bool, default: Rgba) -> Rgba { + match color { + AlacColor::Named(NamedColor::Foreground) => default, + AlacColor::Named(NamedColor::Background) => default, + AlacColor::Named(named) => { + let index = named as u8; + let index = if bold && index < 8 { index + 8 } else { index }; + if index < 16 { + rgba_of(ANSI_PALETTE[index as usize]) + } else { + default } - // RIS:全复位 - b'c' => self.clear_all(), - _ => {} } + AlacColor::Indexed(i) => rgba_of(palette_256(i)), + AlacColor::Spec(Rgb { r, g, b }) => rgba_of((r, g, b)), } } @@ -394,7 +129,8 @@ pub struct TerminalSession { impl TerminalSession { /// 打开 PTY 会话并起输出线程:线程只透传原始字节块,主线程泵喂给 - /// `vte` 解析器(输出实时出现,对齐 xterm 的流式展示)。 + /// 网格解析器(输出实时出现,对齐 xterm 的流式展示)。 + /// 返回会话、输出 channel 与展示用 shell 名。 pub fn new( cols: u16, rows: u16, @@ -476,8 +212,8 @@ impl TerminalSession { pub struct TerminalView { pub session: Option, pub working_dir: String, - screen: TermScreen, - parser: Parser, + term: Term, + processor: Processor, focus_handle: FocusHandle, } @@ -486,32 +222,36 @@ impl TerminalView { let mut view = Self { session: None, working_dir, - screen: TermScreen::new(), - parser: Parser::new(), + term: Term::new( + Config::default(), + &TermDims { + cols: TERMINAL_COLS, + rows: TERMINAL_ROWS, + }, + VoidListener, + ), + processor: Processor::new(), focus_handle: cx.focus_handle(), }; view.attach_session(cx); view } - /// 打开 PTY 会话并起主线程输出泵(channel 收字节块 → `vte` 解析 → notify)。 + /// 打开 PTY 会话并起主线程输出泵(channel 收字节块 → 网格解析 → notify)。 fn attach_session(&mut self, cx: &mut Context) { let shell = Self::resolve_shell(cx); match TerminalSession::new( TERMINAL_COLS as u16, - TERMINAL_ROWS, + TERMINAL_ROWS as u16, &self.working_dir, &shell, ) { Ok((session, rx, displayed)) => { self.session = Some(session); - self.screen.clear_all(); let banner = crate::i18n::menu_text(cx, "terminal.session").replace("{shell}", &displayed); - for c in banner.chars() { - self.screen.put_char(c); - } - self.screen.newline(); + // 首行提示直接画进网格(换行落行)。 + self.advance(format!("{banner}\r\n").as_bytes()); Self::spawn_pump(rx, cx); } Err(_) => { @@ -521,7 +261,14 @@ impl TerminalView { cx.notify(); } - /// 主线程输出泵:后台收字节块,回主线程喂解析器。 + /// 网格推进(含借用拆分:解析器暂存取出再放回)。 + fn advance(&mut self, bytes: &[u8]) { + let mut processor = std::mem::replace(&mut self.processor, Processor::new()); + processor.advance(&mut self.term, bytes); + self.processor = processor; + } + + /// 主线程输出泵:后台收字节块,回主线程喂网格解析器。 fn spawn_pump(rx: mpsc::Receiver>, cx: &mut Context) { let rx = Arc::new(Mutex::new(rx)); cx.spawn(async move |this, cx| loop { @@ -537,12 +284,7 @@ impl TerminalView { Some(bytes) => { if this .update(cx, |view, cx| { - view.parser.advance(&mut view.screen, &bytes); - if view.screen.lines.len() > MAX_TERMINAL_LINES { - let overflow = view.screen.lines.len() - MAX_TERMINAL_LINES; - view.screen.lines.drain(..overflow); - view.screen.row = view.screen.row.saturating_sub(overflow); - } + view.advance(&bytes); cx.notify(); }) .is_err() @@ -594,20 +336,38 @@ impl TerminalView { cx.notify(); } + /// 清屏(等价于 shell `clear`:擦除可见区,保留回滚)。 pub fn clear(&mut self, cx: &mut Context) { - self.screen.clear_all(); - self.screen - .put_str(&crate::i18n::menu_text(cx, "terminal.cleared").to_string()); - self.screen.newline(); + if self.session.is_some() { + self.advance(b"\x1b[2J\x1b[H"); + } cx.notify(); } - /// 用现有工作目录重建 PTY 会话(失败则置空并通知)。 + /// 用现有工作目录重建 PTY 会话与网格(失败则置空并通知)。 pub fn respawn(&mut self, cx: &mut Context) { self.session = None; + self.term = Term::new( + Config::default(), + &TermDims { + cols: TERMINAL_COLS, + rows: TERMINAL_ROWS, + }, + VoidListener, + ); + self.processor = Processor::new(); self.attach_session(cx); } + /// 应用光标键模式:程序用 `\x1bO` 系,否则 `\x1b[` 系。 + fn cursor_key(&self, normal: &[u8], app: &[u8]) -> Vec { + if self.term.mode().contains(TermMode::APP_CURSOR) { + app.to_vec() + } else { + normal.to_vec() + } + } + /// 键盘直输:可打印字符原样写 PTY,控制组合与功能键转义序列, /// 未知键放行(IDEA 快捷键不断)。返回是否消费。 fn handle_key(&mut self, event: &KeyDownEvent) -> bool { @@ -645,12 +405,12 @@ impl TerminalView { "backspace" => vec![0x7f], "tab" => b"\t".to_vec(), "escape" => vec![0x1b], - "up" => b"\x1b[A".to_vec(), - "down" => b"\x1b[B".to_vec(), - "right" => b"\x1b[C".to_vec(), - "left" => b"\x1b[D".to_vec(), - "home" => b"\x1b[H".to_vec(), - "end" => b"\x1b[F".to_vec(), + "up" => self.cursor_key(b"\x1b[A", b"\x1bOA"), + "down" => self.cursor_key(b"\x1b[B", b"\x1bOB"), + "right" => self.cursor_key(b"\x1b[C", b"\x1bOC"), + "left" => self.cursor_key(b"\x1b[D", b"\x1bOD"), + "home" => self.cursor_key(b"\x1b[H", b"\x1bOH"), + "end" => self.cursor_key(b"\x1b[F", b"\x1bOF"), "delete" => b"\x1b[3~".to_vec(), "pageup" => b"\x1b[5~".to_vec(), "pagedown" => b"\x1b[6~".to_vec(), @@ -680,27 +440,43 @@ impl TerminalView { } } -impl TermScreen { - /// 纯文本写入(banner/清空提示用当前默认样式)。 - fn put_str(&mut self, text: &str) { - for c in text.chars() { - if c == '\n' { - self.newline(); - } else { - self.put_char(c); - } - } - } -} - impl Render for TerminalView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let focused = self.focus_handle.is_focused(window); - let total = self.screen.lines.len(); - let start = total.saturating_sub(MAX_RENDER_LINES); let default_fg = ThemeColors::foreground(); let default_bg = ThemeColors::background(); + // 可见区按行分组(`display_iter` 即当前视口)。 + let content = self.term.renderable_content(); + let mut rows: Vec<(i32, Vec<(usize, char, AlacColor, AlacColor, bool)>)> = Vec::new(); + for indexed in content.display_iter { + let line = indexed.point.line.0; + let col = indexed.point.column.0; + if rows.last().map(|(number, _)| *number) != Some(line) { + rows.push((line, Vec::new())); + } + let cell = &indexed.cell; + let inverse = cell.flags.contains(Flags::INVERSE); + let (mut fg, mut bg) = (cell.fg, cell.bg); + if inverse { + std::mem::swap(&mut fg, &mut bg); + } + let bold = cell.flags.contains(Flags::BOLD); + rows.last_mut() + .expect("rows non-empty after push") + .1 + .push((col, cell.c, fg, bg, bold)); + } + let (cursor_line, cursor_col, cursor_visible) = ( + content.cursor.point.line.0, + content.cursor.point.column.0, + focused + && !matches!( + content.cursor.shape, + alacritty_terminal::vte::ansi::CursorShape::Hidden + ), + ); + v_flex() .size_full() .bg(ThemeColors::background()) @@ -727,63 +503,50 @@ impl Render for TerminalView { .text_xs() .text_size(px(crate::settings::get(cx).terminal_font_size)) .font_family("monospace") - .children( - self.screen.lines[start..] - .iter() - .enumerate() - .map(|(ix, line)| { - render_term_line( - start + ix, - line, - focused && start + ix == self.screen.row, - self.screen.col, - default_fg, - default_bg, - ) - }) - .collect::>(), - ), + .children(rows.into_iter().map(|(line, cells)| { + render_term_row( + line, + &cells, + cursor_visible && line == cursor_line, + cursor_col, + default_fg, + default_bg, + ) + })), ) } } -/// 语义颜色解算(加粗配 0-7 自动取高亮 variant,标准终端行为)。 -fn resolve_color(color: TermColor, bold: bool, default: Rgba) -> Rgba { - match color { - TermColor::Default => default, - TermColor::Indexed(i) => { - let i = if bold && i < 8 { i + 8 } else { i }; - rgba_of(palette_256(i)) - } - TermColor::Rgb(r, g, b) => rgba_of((r, g, b)), - } -} - -/// 渲染一行:同样式字符合并为一段;聚焦时在光标处画反色块。 -fn render_term_line( - row: usize, - line: &[StyledChar], +/// 渲染网格一行:同样式字符合并为一段;聚焦时在光标处画反色块。 +fn render_term_row( + line: i32, + cells: &[(usize, char, AlacColor, AlacColor, bool)], cursor_here: bool, cursor_col: usize, default_fg: Rgba, default_bg: Rgba, ) -> gpui_kit::AnyElement { - // 合并同样式段。 - let mut spans: Vec<(SpanStyle, String)> = Vec::new(); - for ch in line { - if let Some((style, text)) = spans.last_mut() { - if *style == ch.style { - text.push(ch.c); + // 合并同样式段(宽字符占位格视为空格参与合并)。 + let mut spans: Vec<(AlacColor, AlacColor, bool, String)> = Vec::new(); + for (col, c, fg, bg, bold) in cells { + let _ = col; + let key = (*fg, *bg, *bold); + if let Some((sfg, sbg, sbold, text)) = spans.last_mut() { + if (*sfg, *sbg, *sbold) == key { + text.push(*c); continue; } } - spans.push((ch.style, ch.c.to_string())); + spans.push((*fg, *bg, *bold, c.to_string())); } - let total: usize = spans.iter().map(|(_, text)| text.chars().count()).sum(); + let total: usize = spans + .iter() + .map(|(_, _, _, text)| text.chars().count()) + .sum(); // 光标超出文本末尾时在行尾补空格块。 let trailing = cursor_here && cursor_col >= total; let mut row_el = h_flex() - .id(row) + .id(format!("term-row-{line}")) .w_full() .items_center() .text_xs() @@ -794,14 +557,16 @@ fn render_term_line( .into_any_element(); } let mut col = 0; - for (style, text) in &spans { - let fg = resolve_color(style.fg, style.bold, default_fg); - let bg = resolve_color(style.bg, false, default_bg); + for (fg, bg, bold, text) in &spans { + let fg_resolved = resolve_color(*fg, *bold, default_fg); + let bg_resolved = resolve_color(*bg, false, default_bg); let chunk = |text: String| { div() - .text_color(fg) - .when(style.bg != TermColor::Default, |el| el.bg(bg)) - .when(style.bold, |el| el.font_weight(FontWeight::BOLD)) + .text_color(fg_resolved) + .when(*bg != AlacColor::Named(NamedColor::Background), |el| { + el.bg(bg_resolved) + }) + .when(*bold, |el| el.font_weight(FontWeight::BOLD)) .child(text) }; if trailing { @@ -827,7 +592,7 @@ fn render_term_line( row_el = row_el.child( div() .text_color(default_bg) - .bg(fg) + .bg(fg_resolved) .child(cursor_char.unwrap_or(' ').to_string()), ); if !after.is_empty() { From f8356560c8b63e06f3dd2f76c8ff6ede4b4fa315 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 11:37:00 +0800 Subject: [PATCH 48/59] =?UTF-8?q?fix(linux):=20=E7=BB=88=E7=AB=AF=E5=85=89?= =?UTF-8?q?=E6=A0=87=E6=95=B4=E8=A1=8C=E5=8F=AA=E7=94=BB=E4=B8=80=E6=AC=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- linux/src/workbench/terminal.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/linux/src/workbench/terminal.rs b/linux/src/workbench/terminal.rs index 48163bc62..ae64d4d13 100644 --- a/linux/src/workbench/terminal.rs +++ b/linux/src/workbench/terminal.rs @@ -557,6 +557,7 @@ fn render_term_row( .into_any_element(); } let mut col = 0; + let mut cursor_drawn = false; for (fg, bg, bold, text) in &spans { let fg_resolved = resolve_color(*fg, *bold, default_fg); let bg_resolved = resolve_color(*bg, false, default_bg); @@ -569,8 +570,11 @@ fn render_term_row( .when(*bold, |el| el.font_weight(FontWeight::BOLD)) .child(text) }; - if trailing { + let span_len = text.chars().count(); + // 光标块整行只画一次:完全在光标前/后的段原样输出。 + if trailing || col + span_len <= cursor_col || cursor_drawn { row_el = row_el.child(chunk(text.clone())); + col += span_len; continue; } let mut before = String::new(); @@ -595,6 +599,7 @@ fn render_term_row( .bg(fg_resolved) .child(cursor_char.unwrap_or(' ').to_string()), ); + cursor_drawn = true; if !after.is_empty() { row_el = row_el.child(chunk(after)); } From 6bf80b2b9709dbaf3fa2553962432e1cf324111b Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 11:54:28 +0800 Subject: [PATCH 49/59] =?UTF-8?q?feat(linux):=20=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E8=BE=93=E5=87=BA=E8=B7=9F=E9=9A=8F=E6=9C=AB=E5=B0=BE=E4=B8=8E?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=8D=A2=E8=A1=8C=E5=AF=B9=E9=BD=90=20Tauri?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Run 输出加滚动跟随开关(默认开,落盘),新增输出滚到底 - 输出行显式换行模式 + 行首缩进保留(pre-wrap 口径) - 终端网格行视觉换行,不再裁掉超宽行 --- linux/src/settings.rs | 3 + linux/src/workbench/bottom_panel.rs | 150 +++++++++++++++++++--------- linux/src/workbench/terminal.rs | 1 + 3 files changed, 106 insertions(+), 48 deletions(-) diff --git a/linux/src/settings.rs b/linux/src/settings.rs index 1a10b668d..8114e0aa5 100644 --- a/linux/src/settings.rs +++ b/linux/src/settings.rs @@ -172,6 +172,8 @@ pub struct Settings { 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, @@ -279,6 +281,7 @@ impl Default for Settings { 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: "zh-CN".to_string(), reduce_motion: false, diff --git a/linux/src/workbench/bottom_panel.rs b/linux/src/workbench/bottom_panel.rs index b49843ac1..3f392751f 100644 --- a/linux/src/workbench/bottom_panel.rs +++ b/linux/src/workbench/bottom_panel.rs @@ -7,7 +7,8 @@ use gpui_kit::component::{h_flex, v_flex, Disableable as _, Icon, Sizable as _}; use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::{ div, px, AnyElement, AppContext as _, Context, Entity, FontWeight, InteractiveElement as _, - IntoElement, ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Window, + IntoElement, ParentElement as _, Render, ScrollHandle, StatefulInteractiveElement as _, + Styled as _, Window, }; use crate::core::CoreClient; @@ -131,6 +132,12 @@ pub struct BottomPanelView { pub(crate) run_output: Vec, /// 是否有进程在跑。 pub(crate) run_running: bool, + /// 输出跟随末尾(对齐 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 页)。 @@ -173,6 +180,9 @@ impl BottomPanelView { selected_run_config: None, run_output: Vec::new(), run_running: false, + 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, @@ -687,7 +697,8 @@ impl BottomPanelView { .into_any_element() } - /// Run 窗格头部:运行/停止 + 重扫 + 清空 + 最小化(对齐 Tauri RunPane 头)。 + /// Run 窗格头部:运行/停止 + 重扫 + 跟随末尾 + 清空 + 最小化 + ///(对齐 Tauri RunPane 头)。 fn render_run_header(&self, cx: &mut Context) -> AnyElement { let running = self.run_running; let status = running.then(|| { @@ -703,46 +714,60 @@ impl BottomPanelView { self.run_project_name ); let can_run = !matches!(self.run_state, RunProjectState::Loading); - self.render_pane_header( - IconName::Play, - title, - status, - 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), - ), - 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(); - }, - ), - ], + 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 头)。 @@ -805,9 +830,8 @@ impl BottomPanelView { ) } - /// Run 面板:Tauri RunPane 子集——顶部状态行(工程名+状态+运行/停止+ - /// 重扫+清空)+ 配置列表(选中高亮)+ 输出区(monospace)。 - fn render_run_panel(&self, cx: &mut Context) -> AnyElement { + /// 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 => { @@ -936,9 +960,16 @@ impl BottomPanelView { } else { self.run_output[start..] .iter() - .map(|line| div().child(line.clone()).into_any_element()) + .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() @@ -962,7 +993,7 @@ impl BottomPanelView { div() .flex_1() .h_full() - .overflow_y_scrollbar() + .vertical_scrollbar(&self.run_scroll) .p_2() .font_family("monospace") .text_xs() @@ -985,7 +1016,7 @@ impl BottomPanelView { } else { self.maven_output[start..] .iter() - .map(|line| div().child(line.clone()).into_any_element()) + .map(|line| render_output_line(line)) .collect() }; div() @@ -1735,7 +1766,30 @@ fn run_steps_blocking( } } -/// 输出行入列(超限丢最旧)。 +/// 行首空白转不换行空格: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 { diff --git a/linux/src/workbench/terminal.rs b/linux/src/workbench/terminal.rs index ae64d4d13..471b03b72 100644 --- a/linux/src/workbench/terminal.rs +++ b/linux/src/workbench/terminal.rs @@ -549,6 +549,7 @@ fn render_term_row( .id(format!("term-row-{line}")) .w_full() .items_center() + .flex_wrap() .text_xs() .font_family("monospace"); if spans.is_empty() && !trailing { From 164aa2e6142f454ece15561fc0dc9b65ca3713b3 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 12:11:26 +0800 Subject: [PATCH 50/59] fix(windows): normalize persisted display language --- .../settings/lib/settings-normalization.test.ts | 14 ++++++++++++++ .../settings/lib/settings-normalization.ts | 5 +++++ windows/tauri/src/i18n/locale.ts | 10 ++++++++++ 3 files changed, 29 insertions(+) diff --git a/windows/tauri/src/features/settings/lib/settings-normalization.test.ts b/windows/tauri/src/features/settings/lib/settings-normalization.test.ts index 374f253d4..5a35c8717 100644 --- a/windows/tauri/src/features/settings/lib/settings-normalization.test.ts +++ b/windows/tauri/src/features/settings/lib/settings-normalization.test.ts @@ -51,6 +51,20 @@ describe("IDEA file icon theme normalization", () => { }); }); +describe("display language normalization", () => { + test("keeps the settings selector aligned with the persisted locale", () => { + const settings = getDefaultSettingsSnapshot(); + settings.displayLanguage = "zh" as typeof settings.displayLanguage; + + expect(normalizeSettings(settings).displayLanguage).toBe("zh-CN"); + }); + + test("normalizes legacy English locale aliases", () => { + // Simulate a legacy value that bypassed the Settings type at runtime. + expect(normalizeSettingValue("displayLanguage", "en_US" as never)).toBe("en-US"); + }); +}); + describe("Windows New UI defaults", () => { test("uses Chinese and the native CJK UI typography for new settings", () => { const settings = getDefaultSettingsSnapshot(); diff --git a/windows/tauri/src/features/settings/lib/settings-normalization.ts b/windows/tauri/src/features/settings/lib/settings-normalization.ts index 63e0de0fd..70b81df5e 100644 --- a/windows/tauri/src/features/settings/lib/settings-normalization.ts +++ b/windows/tauri/src/features/settings/lib/settings-normalization.ts @@ -23,6 +23,7 @@ import { } from "@/features/layout/config/item-order"; import { normalizeUiFontSize } from "@/features/settings/lib/ui-font-size"; import type { Settings, SettingsSection } from "@/features/settings/types/settings.types"; +import { normalizeDisplayLanguage } from "@/i18n/locale"; const AI_MODEL_MIGRATIONS: Record> = { anthropic: { @@ -514,6 +515,9 @@ export function normalizeSettings(settings: Settings): Settings { normalizedSettings.windowChromeDensity = normalizeWindowChromeDensity( (normalizedSettings as { windowChromeDensity?: unknown }).windowChromeDensity, ); + normalizedSettings.displayLanguage = normalizeDisplayLanguage( + (normalizedSettings as { displayLanguage?: unknown }).displayLanguage, + ); normalizedSettings.fileTreeSortOrder = normalizeFileTreeSortOrder( (normalizedSettings as { fileTreeSortOrder?: unknown }).fileTreeSortOrder, ); @@ -584,6 +588,7 @@ export function normalizeSettingValue( if (key === "aiCommit") return normalizeCommitAI(value) as Settings[K]; if (key === "gitFetchSubmodules") return (["inherit", "no", "onDemand", "yes"].includes(String(value)) ? value : "inherit") as Settings[K]; if (key === "gitFetchTags") return (["inherit", "all", "none", "prune"].includes(String(value)) ? value : "inherit") as Settings[K]; + if (key === "displayLanguage") return normalizeDisplayLanguage(value) as Settings[K]; if (key === "uiFontSize") { return normalizeUiFontSize(value as number) as Settings[K]; } diff --git a/windows/tauri/src/i18n/locale.ts b/windows/tauri/src/i18n/locale.ts index da843cf5b..b271ac322 100644 --- a/windows/tauri/src/i18n/locale.ts +++ b/windows/tauri/src/i18n/locale.ts @@ -3,6 +3,16 @@ export const DISPLAY_LANGUAGES = ["en-US", "zh-CN"] as const; export type DisplayLanguage = (typeof DISPLAY_LANGUAGES)[number]; +/** Convert persisted and legacy locale values to the settings option values. */ +export function normalizeDisplayLanguage(value: unknown): DisplayLanguage { + if (typeof value !== "string") return "zh-CN"; + + const language = value.trim().toLowerCase().replace("_", "-"); + if (language.startsWith("zh")) return "zh-CN"; + if (language.startsWith("en")) return "en-US"; + return "zh-CN"; +} + const catalogs = { "en-US": { ...aiCommitEnglish, From 1af5796440c14621375426aa733d112a70b131e5 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 12:13:03 +0800 Subject: [PATCH 51/59] Revert "fix(windows): normalize persisted display language" This reverts commit 164aa2e6142f454ece15561fc0dc9b65ca3713b3. --- .../settings/lib/settings-normalization.test.ts | 14 -------------- .../settings/lib/settings-normalization.ts | 5 ----- windows/tauri/src/i18n/locale.ts | 10 ---------- 3 files changed, 29 deletions(-) diff --git a/windows/tauri/src/features/settings/lib/settings-normalization.test.ts b/windows/tauri/src/features/settings/lib/settings-normalization.test.ts index 5a35c8717..374f253d4 100644 --- a/windows/tauri/src/features/settings/lib/settings-normalization.test.ts +++ b/windows/tauri/src/features/settings/lib/settings-normalization.test.ts @@ -51,20 +51,6 @@ describe("IDEA file icon theme normalization", () => { }); }); -describe("display language normalization", () => { - test("keeps the settings selector aligned with the persisted locale", () => { - const settings = getDefaultSettingsSnapshot(); - settings.displayLanguage = "zh" as typeof settings.displayLanguage; - - expect(normalizeSettings(settings).displayLanguage).toBe("zh-CN"); - }); - - test("normalizes legacy English locale aliases", () => { - // Simulate a legacy value that bypassed the Settings type at runtime. - expect(normalizeSettingValue("displayLanguage", "en_US" as never)).toBe("en-US"); - }); -}); - describe("Windows New UI defaults", () => { test("uses Chinese and the native CJK UI typography for new settings", () => { const settings = getDefaultSettingsSnapshot(); diff --git a/windows/tauri/src/features/settings/lib/settings-normalization.ts b/windows/tauri/src/features/settings/lib/settings-normalization.ts index 70b81df5e..63e0de0fd 100644 --- a/windows/tauri/src/features/settings/lib/settings-normalization.ts +++ b/windows/tauri/src/features/settings/lib/settings-normalization.ts @@ -23,7 +23,6 @@ import { } from "@/features/layout/config/item-order"; import { normalizeUiFontSize } from "@/features/settings/lib/ui-font-size"; import type { Settings, SettingsSection } from "@/features/settings/types/settings.types"; -import { normalizeDisplayLanguage } from "@/i18n/locale"; const AI_MODEL_MIGRATIONS: Record> = { anthropic: { @@ -515,9 +514,6 @@ export function normalizeSettings(settings: Settings): Settings { normalizedSettings.windowChromeDensity = normalizeWindowChromeDensity( (normalizedSettings as { windowChromeDensity?: unknown }).windowChromeDensity, ); - normalizedSettings.displayLanguage = normalizeDisplayLanguage( - (normalizedSettings as { displayLanguage?: unknown }).displayLanguage, - ); normalizedSettings.fileTreeSortOrder = normalizeFileTreeSortOrder( (normalizedSettings as { fileTreeSortOrder?: unknown }).fileTreeSortOrder, ); @@ -588,7 +584,6 @@ export function normalizeSettingValue( if (key === "aiCommit") return normalizeCommitAI(value) as Settings[K]; if (key === "gitFetchSubmodules") return (["inherit", "no", "onDemand", "yes"].includes(String(value)) ? value : "inherit") as Settings[K]; if (key === "gitFetchTags") return (["inherit", "all", "none", "prune"].includes(String(value)) ? value : "inherit") as Settings[K]; - if (key === "displayLanguage") return normalizeDisplayLanguage(value) as Settings[K]; if (key === "uiFontSize") { return normalizeUiFontSize(value as number) as Settings[K]; } diff --git a/windows/tauri/src/i18n/locale.ts b/windows/tauri/src/i18n/locale.ts index b271ac322..da843cf5b 100644 --- a/windows/tauri/src/i18n/locale.ts +++ b/windows/tauri/src/i18n/locale.ts @@ -3,16 +3,6 @@ export const DISPLAY_LANGUAGES = ["en-US", "zh-CN"] as const; export type DisplayLanguage = (typeof DISPLAY_LANGUAGES)[number]; -/** Convert persisted and legacy locale values to the settings option values. */ -export function normalizeDisplayLanguage(value: unknown): DisplayLanguage { - if (typeof value !== "string") return "zh-CN"; - - const language = value.trim().toLowerCase().replace("_", "-"); - if (language.startsWith("zh")) return "zh-CN"; - if (language.startsWith("en")) return "en-US"; - return "zh-CN"; -} - const catalogs = { "en-US": { ...aiCommitEnglish, From d30b862b7e0522db1297cab45f2017878558f408 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 12:13:53 +0800 Subject: [PATCH 52/59] fix(linux): normalize display language setting --- linux/src/settings.rs | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/linux/src/settings.rs b/linux/src/settings.rs index 8114e0aa5..a3b9405c6 100644 --- a/linux/src/settings.rs +++ b/linux/src/settings.rs @@ -148,6 +148,18 @@ impl Default for AiCommitSettings { } } +/// 将显示语言值规范化为设置下拉框使用的稳定标识。 +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")] @@ -283,7 +295,7 @@ impl Default for Settings { terminal_default_shell_id: String::new(), run_scroll_to_end: true, ui_font_size: 13.0, - display_language: "zh-CN".to_string(), + display_language: normalized_display_language("zh-CN"), reduce_motion: false, show_status_bar: true, show_tab_icons: true, @@ -403,7 +415,9 @@ pub fn load() -> Settings { let Ok(text) = std::fs::read_to_string(&path) else { return Settings::default(); }; - serde_json::from_str::(&text).unwrap_or_default() + let mut settings = serde_json::from_str::(&text).unwrap_or_default(); + settings.display_language = normalized_display_language(&settings.display_language); + settings } /// 将设置写回磁盘(临时文件 + 原子替换)。 @@ -434,6 +448,8 @@ pub fn init(cx: &mut App) { /// 修改设置:写全局、落盘并刷新所有窗口。 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(); @@ -468,3 +484,20 @@ 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"); + } +} From 419727629c46c69df6c7fbd2cbeeb3dbcaf158ed Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 12:43:58 +0800 Subject: [PATCH 53/59] feat(linux): replicate diagnostics panel behavior --- linux/src/workbench/bottom_panel.rs | 127 ++++++++++++++++++++++++---- linux/src/workbench/view.rs | 17 +++- 2 files changed, 125 insertions(+), 19 deletions(-) diff --git a/linux/src/workbench/bottom_panel.rs b/linux/src/workbench/bottom_panel.rs index 3f392751f..f5c2d82c7 100644 --- a/linux/src/workbench/bottom_panel.rs +++ b/linux/src/workbench/bottom_panel.rs @@ -5,6 +5,7 @@ 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 _, @@ -102,6 +103,24 @@ pub enum BottomTab { 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 }, +} + /// Git 提交记录的一行:短 hash + 首行 message,只读展示不跳转。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct GitLogEntry { @@ -114,7 +133,7 @@ pub struct BottomPanelView { pub is_collapsed: bool, pub height: f32, pub terminal: Entity, - pub diagnostics: Vec, + pub diagnostics: Vec, /// 终端工作目录;GitLog 面板取数时作为 `git -C` 目标。 pub working_dir: String, /// 运行历史(`TerminalView::send_command` 被调用时由宿主经 @@ -162,6 +181,8 @@ pub struct BottomPanelView { pub git_log_error: Option, } +impl EventEmitter for BottomPanelView {} + impl BottomPanelView { pub fn new(working_dir: String, cx: &mut Context) -> Self { let terminal = cx.new(|cx| TerminalView::new(working_dir.clone(), cx)); @@ -201,6 +222,93 @@ impl BottomPanelView { !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(); @@ -1156,22 +1264,7 @@ impl Render for BottomPanelView { )], cx, ), - div() - .size_full() - .p_3() - .overflow_y_scrollbar() - .text_xs() - .text_color(ThemeColors::text_muted()) - .child(if self.diagnostics.is_empty() { - crate::i18n::menu_text(cx, "diagnostics.empty").to_string() - } else { - format!( - "{} ({})", - crate::i18n::menu_text(cx, "workbench.diagnostics"), - self.diagnostics.len() - ) - }) - .into_any_element(), + self.render_diagnostics_panel(cx), ), BottomTab::GitLog => ( self.render_pane_header( diff --git a/linux/src/workbench/view.rs b/linux/src/workbench/view.rs index c1bd5cc96..a53727921 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -22,7 +22,7 @@ use crate::theme::ThemeColors; use crate::workbench::activity_rail::{ ActivityRailEvent, ActivityRailView, PluginActivityRailView, PluginRailEvent, }; -use crate::workbench::bottom_panel::{BottomPanelView, BottomTab}; +use crate::workbench::bottom_panel::{BottomPanelEvent, BottomPanelView, BottomTab}; use crate::workbench::branch_manager::{BranchManagerEvent, BranchManagerView}; use crate::workbench::command_palette::{CommandPaletteEvent, CommandPaletteModal}; use crate::workbench::editor::{EditorTabEvent, EditorView}; @@ -320,7 +320,19 @@ impl WorkbenchView { }, ); - // 4. 订阅右侧插件活动栏事件 + // 4. 订阅底部诊断面板事件 + let sub_bottom_panel = cx.subscribe( + &bottom_panel, + |this, _panel, event: &BottomPanelEvent, cx| match event { + BottomPanelEvent::OpenFile { path, line } => { + this.open_file(path, cx); + this.pending_goto_line = Some(*line); + cx.notify(); + } + }, + ); + + // 5. 订阅右侧插件活动栏事件 let sub_plugin_rail = cx.subscribe( &plugin_rail, |this, _rail, event: &PluginRailEvent, cx| match event { @@ -808,6 +820,7 @@ impl WorkbenchView { sub_sidebar, obs_sidebar, sub_rail, + sub_bottom_panel, sub_plugin_rail, sub_notifications, sub_extensions, From 526dd816284907a360fd74e4b873c6096a2c361a Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 13:05:53 +0800 Subject: [PATCH 54/59] =?UTF-8?q?feat(linux):=20=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E6=9C=8D=E5=8A=A1=E5=99=A8=E5=B9=B6=E8=BE=93?= =?UTF-8?q?=E5=87=BA=E8=AF=8A=E6=96=AD=E9=9D=A2=E6=9D=BF=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Linux 端此前只有诊断面板 UI,没有任何数据生产者,面板恒为空。本次把编辑器 文档变化接到共享 Rust Core 的 LSP 引擎,让诊断面板拿到真实的语言服务器结果。 改动点: - 新增 `linux/src/lsp.rs`:语言/服务器选择(rust-analyzer、clangd、pyright、 typescript-language-server、gopls、jdtls)、PATH 可执行文件探测、工作区 路径与 `file://` URI 的双向编解码(含百分号转义)、`lsp.startServer` / `lsp.syncDocument` / `lsp.pollEvents` 的请求载荷,以及把 `diagnostics` 事件投影为面板条目的解析逻辑;并带 4 个单元测试。 - `linux/src/workbench/editor.rs`:`EditorTabEvent` 新增 `DocumentChanged`, 在打开文件、切换标签和编辑文本时广播当前活动文档。 - `linux/src/workbench/view.rs`:工作台持有 LSP 会话、待同步文档、轮询任务和 诊断缓存;订阅编辑器文档事件后按需启动会话(同 provider 只启一个)、 启动后 flush 暂存文档、以 400ms 有界间隔轮询事件,并把诊断聚合排序后交给 底部面板;会话终态或轮询失败后清理会话状态。 - `linux/src/workbench/bottom_panel.rs`:新增 `set_diagnostics` 供工作台写入, 清空按钮改为发出 `ClearDiagnostics` 事件,让宿主同步丢弃缓存,避免下一轮 诊断把已清空内容重新填回。 - `linux/src/lib.rs`、`linux/src/main.rs`:声明 `lsp` 模块。 验证: - `cargo build --manifest-path linux/Cargo.toml` 通过。 - `cargo test --manifest-path linux/Cargo.toml --bin lithe-linux` 23 项通过 (含 4 项新增 lsp 测试)。 - 用临时 smoke 程序直接调用 Core 的 `lsp.startServer`/`lsp.syncDocument`/ `lsp.pollEvents` 驱动 rust-analyzer,确认能收到带 `range.start.line`、 `utf16Column`、`severity`、`source` 的 diagnostics 事件,与投影逻辑一致。 --- linux/src/lib.rs | 1 + linux/src/lsp.rs | 391 ++++++++++++++++++++++++++++ linux/src/main.rs | 1 + linux/src/workbench/bottom_panel.rs | 9 + linux/src/workbench/editor.rs | 17 ++ linux/src/workbench/view.rs | 286 +++++++++++++++++++- 6 files changed, 702 insertions(+), 3 deletions(-) create mode 100644 linux/src/lsp.rs diff --git a/linux/src/lib.rs b/linux/src/lib.rs index f8775d333..559e4cf94 100644 --- a/linux/src/lib.rs +++ b/linux/src/lib.rs @@ -1,5 +1,6 @@ pub mod core; pub mod i18n; +pub mod lsp; pub mod settings; pub mod theme; pub mod workbench; diff --git a/linux/src/lsp.rs b/linux/src/lsp.rs new file mode 100644 index 000000000..c9ebc6f4d --- /dev/null +++ b/linux/src/lsp.rs @@ -0,0 +1,391 @@ +//! Linux 端 LSP 接线:把编辑器文档同步给 Core 管理的语言服务器,并把 +//! `lsp.pollEvents` 返回的诊断映射到诊断面板。 +//! +//! Core 拥有进程、JSON-RPC framing、生命周期和诊断状态;本模块只做 +//! 平台侧的语言/可执行文件探测、路径与 URI 规范化,以及事件投影。 + +use std::collections::HashMap; + +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], +} + +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: "jdtls", + 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 +} + +/// 把相对工作区路径解析成绝对路径;已经是绝对路径时原样返回。 +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 mut arguments: Vec = provider + .arguments + .iter() + .map(|value| value.to_string()) + .collect(); + if provider.id == "jdtls" { + // jdtls 要求显式 data 目录;configuration 目录由发行包脚本补齐。 + arguments.push("-data".to_string()); + arguments.push(cache_directory.to_string()); + } + 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 只允许在收到终态事件后销毁会话。 +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) +} + +/// 把一次 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 +} + +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("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); + } +} diff --git a/linux/src/main.rs b/linux/src/main.rs index 49095f030..d89dd7899 100644 --- a/linux/src/main.rs +++ b/linux/src/main.rs @@ -2,6 +2,7 @@ mod core; mod i18n; +mod lsp; mod settings; mod theme; mod workbench; diff --git a/linux/src/workbench/bottom_panel.rs b/linux/src/workbench/bottom_panel.rs index f5c2d82c7..3079d014a 100644 --- a/linux/src/workbench/bottom_panel.rs +++ b/linux/src/workbench/bottom_panel.rs @@ -119,6 +119,8 @@ pub struct DiagnosticEntry { #[derive(Debug, Clone)] pub enum BottomPanelEvent { OpenFile { path: String, line: u32 }, + /// 面板清空按钮:请求宿主同步丢弃缓存的 LSP 诊断。 + ClearDiagnostics, } /// Git 提交记录的一行:短 hash + 首行 message,只读展示不跳转。 @@ -330,6 +332,12 @@ impl BottomPanelView { 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(); @@ -1259,6 +1267,7 @@ impl Render for BottomPanelView { cx, |this, _window, cx| { this.diagnostics.clear(); + cx.emit(BottomPanelEvent::ClearDiagnostics); cx.notify(); }, )], diff --git a/linux/src/workbench/editor.rs b/linux/src/workbench/editor.rs index 282778855..10e5fbbdf 100644 --- a/linux/src/workbench/editor.rs +++ b/linux/src/workbench/editor.rs @@ -129,6 +129,7 @@ impl EditorView { } } self.active_tab_index = Some(pos); + self.publish_active_document(cx); cx.notify(); return; } @@ -150,6 +151,7 @@ impl EditorView { self.active_tab_index = Some(self.tabs.len() - 1); self.sync_needed = true; + self.publish_active_document(cx); cx.notify(); } @@ -206,6 +208,18 @@ impl EditorView { 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(), + }); } /// 编辑器文本变化后回写标签内容并标记为已修改。 @@ -229,6 +243,7 @@ impl EditorView { } self.redo_stack.clear(); } + self.publish_active_document(cx); cx.notify(); } @@ -993,6 +1008,8 @@ impl EditorView { #[derive(Debug, Clone)] pub enum EditorTabEvent { OpenInTerminal { dir: String }, + /// 活动标签的内容或身份发生变化,父视图据此同步 LSP 文档态。 + DocumentChanged { path: String, text: String }, } impl EventEmitter for EditorView {} diff --git a/linux/src/workbench/view.rs b/linux/src/workbench/view.rs index a53727921..6d00bcdc0 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -13,16 +13,20 @@ use gpui_kit::{ MouseMoveEvent, MouseUpEvent, ParentElement as _, Render, Styled as _, Subscription, Window, }; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::rc::Rc; +use std::time::Duration; use crate::core::CoreClient; +use crate::lsp::{self, LanguageProvider}; use crate::settings; use crate::theme::ThemeColors; use crate::workbench::activity_rail::{ ActivityRailEvent, ActivityRailView, PluginActivityRailView, PluginRailEvent, }; -use crate::workbench::bottom_panel::{BottomPanelEvent, BottomPanelView, BottomTab}; +use crate::workbench::bottom_panel::{ + BottomPanelEvent, BottomPanelView, BottomTab, DiagnosticEntry, +}; use crate::workbench::branch_manager::{BranchManagerEvent, BranchManagerView}; use crate::workbench::command_palette::{CommandPaletteEvent, CommandPaletteModal}; use crate::workbench::editor::{EditorTabEvent, EditorView}; @@ -50,6 +54,14 @@ pub enum RightToolView { Extensions, } +/// 语言服务器尚未就绪时暂存的一份文档同步请求。 +#[derive(Debug, Clone)] +struct PendingDocument { + uri: String, + language_id: String, + text: String, +} + /// 主工作台视图。 pub struct WorkbenchView { /// 项目工作区根路径 @@ -101,6 +113,18 @@ pub struct WorkbenchView { pub extensions: Entity, /// 通知诊断行点击后的待跳转行号(1 起,文件加载完成后在 render 应用) pending_goto_line: Option, + /// LSP:provider id → Core 会话 id(非空即已启动)。 + lsp_sessions: HashMap, + /// LSP:正在启动中的 provider id,避免重复 `lsp.startServer`。 + lsp_starting: HashSet, + /// LSP:正在轮询的会话 id,保证每个会话只有一个 poll 循环。 + lsp_polling: HashSet, + /// LSP:会话中已同步文档的版本号(`session\0uri` → version)。 + lsp_document_versions: HashMap, + /// LSP:会话未就绪时暂存的待同步文档(provider id → 文档)。 + lsp_pending_documents: HashMap>, + /// LSP:工作区相对路径 → 该文件最新诊断,供底部面板聚合展示。 + lsp_diagnostics: HashMap>, /// 侧边栏面板 (Files / Git / Search) pub sidebar: Entity, /// 多窗格布局模型(`Split{Horizontal}` 左右并排,`{Vertical}` 上下堆叠) @@ -329,6 +353,9 @@ impl WorkbenchView { this.pending_goto_line = Some(*line); cx.notify(); } + BottomPanelEvent::ClearDiagnostics => { + this.lsp_diagnostics.clear(); + } }, ); @@ -642,7 +669,7 @@ impl WorkbenchView { }, ); - // 8b. 订阅编辑器标签页事件(在终端中打开) + // 8b. 订阅编辑器标签页事件(在终端中打开 / 文档变化同步 LSP) let sub_editor_tab = cx.subscribe( &editor, @@ -650,6 +677,9 @@ impl WorkbenchView { EditorTabEvent::OpenInTerminal { dir } => { this.send_terminal_command(&format!("cd \"{dir}\""), cx); } + EditorTabEvent::DocumentChanged { path, text } => { + this.lsp_on_document_changed(path.clone(), text.clone(), cx); + } }, ); @@ -795,6 +825,12 @@ impl WorkbenchView { show_branch_manager: false, right_tool: None, pending_goto_line: None, + lsp_sessions: HashMap::new(), + lsp_starting: HashSet::new(), + lsp_polling: HashSet::new(), + lsp_document_versions: HashMap::new(), + lsp_pending_documents: HashMap::new(), + lsp_diagnostics: HashMap::new(), toolbar, activity_rail, plugin_rail, @@ -1089,6 +1125,247 @@ impl WorkbenchView { self.active_editor() } + /// 编辑器文档变化:选择语言服务器、按需启动会话并同步全文。 + /// 未知语言或本机无对应可执行文件时静默跳过,不影响编辑。 + fn lsp_on_document_changed(&mut self, path: String, text: String, cx: &mut Context) { + let Some((provider, executable)) = lsp::resolve_provider(&path) else { + return; + }; + let root = self.workspace_root.clone(); + let uri = lsp::file_uri(&root, &path); + let language_id = lsp::language_id_for_path(&path, provider).to_string(); + + if let Some(session) = self.lsp_sessions.get(provider.id).cloned() { + self.lsp_sync_document(&session, uri, language_id, text, cx); + return; + } + + // 会话未就绪:先暂存最新全文,会话 ready 后统一 flush。 + let pending = self + .lsp_pending_documents + .entry(provider.id.to_string()) + .or_default(); + if let Some(existing) = pending.iter_mut().find(|document| document.uri == uri) { + existing.text = text; + } else { + pending.push(PendingDocument { + uri, + language_id, + text, + }); + } + self.lsp_start_server(provider, executable, root, cx); + } + + /// 启动一个语言服务器会话;同一 provider 同时只允许一个启动任务。 + fn lsp_start_server( + &mut self, + provider: LanguageProvider, + executable: String, + root: String, + cx: &mut Context, + ) { + if self.lsp_starting.contains(provider.id) || self.lsp_sessions.contains_key(provider.id) { + return; + } + self.lsp_starting.insert(provider.id.to_string()); + + let client = self.client.clone(); + let cache_directory = format!("{root}/.lithe/lsp/{}", provider.id); + let payload = lsp::start_payload(provider, &executable, &root, &cache_directory); + cx.spawn(async move |this, cx| { + let result = client + .execute::<_, serde_json::Value>(&cx, "lsp.startServer", payload) + .await; + let _ = this.update(cx, |this, cx| { + this.lsp_starting.remove(provider.id); + match result { + Ok(value) => match lsp::parse_session_id(&value) { + Some(session) => { + this.lsp_sessions + .insert(provider.id.to_string(), session.clone()); + this.lsp_start_poll(provider.id, session.clone(), cx); + this.lsp_flush_pending(provider.id, &session, cx); + } + None => { + this.lsp_push_error(provider.id, "lsp.startServer 未返回 sessionId", cx) + } + }, + Err(error) => this.lsp_push_error(provider.id, &error, cx), + } + cx.notify(); + }); + }) + .detach(); + } + + /// 会话建立后把所有暂存文档送入 Core。 + fn lsp_flush_pending(&mut self, provider_id: &str, session: &str, cx: &mut Context) { + let Some(pending) = self.lsp_pending_documents.remove(provider_id) else { + return; + }; + for document in pending { + self.lsp_sync_document( + session, + document.uri, + document.language_id, + document.text, + cx, + ); + } + } + + /// 把一份全文同步给 Core;版本号在响应后写回,供后续状态查询使用。 + fn lsp_sync_document( + &mut self, + session: &str, + uri: String, + language_id: String, + text: String, + cx: &mut Context, + ) { + let client = self.client.clone(); + let key = format!("{session}\u{0}{uri}"); + let payload = lsp::sync_payload(session, &uri, &language_id, &text); + cx.spawn(async move |this, cx| { + let result = client + .execute::<_, serde_json::Value>(&cx, "lsp.syncDocument", payload) + .await; + if let Ok(value) = result { + let version = value + .get("documentVersion") + .and_then(serde_json::Value::as_i64); + let _ = this.update(cx, |this, _cx| { + match version { + Some(version) => { + this.lsp_document_versions.insert(key, version); + } + // 保留会话-文档映射,便于后续清理时识别归属。 + None => { + this.lsp_document_versions.entry(key).or_insert(0); + } + } + }); + } + }) + .detach(); + } + + /// 为会话启动唯一的事件轮询循环,直至服务器停止或命令失败。 + fn lsp_start_poll( + &mut self, + provider_id: &'static str, + session: String, + cx: &mut Context, + ) { + if !self.lsp_polling.insert(session.clone()) { + return; + } + let client = self.client.clone(); + let root = self.workspace_root.clone(); + cx.spawn(async move |this, cx| { + loop { + let payload = lsp::poll_payload(&session); + let result = client + .execute::<_, serde_json::Value>(&cx, "lsp.pollEvents", payload) + .await; + let finished = match result { + Ok(value) => { + let diagnostics = lsp::diagnostics_from_poll(&value, &root); + let finished = lsp::session_finished(&value); + let provider = provider_id.to_string(); + let session_key = session.clone(); + let _ = this.update(cx, |this, cx| { + if !diagnostics.is_empty() { + this.lsp_apply_diagnostics(diagnostics, cx); + } + if finished { + this.lsp_forget_session(&provider, &session_key, cx); + } + }); + finished + } + Err(_) => true, + }; + if finished { + break; + } + cx.background_executor() + .timer(Duration::from_millis(400)) + .await; + } + let _ = this.update(cx, |this, _cx| { + this.lsp_polling.remove(&session); + }); + }) + .detach(); + } + + /// 会话终止后的清理:移除会话、轮询标记、文档版本与错误占位。 + fn lsp_forget_session(&mut self, provider_id: &str, session: &str, cx: &mut Context) { + self.lsp_sessions.remove(provider_id); + self.lsp_polling.remove(session); + let prefix = format!("{session}\u{0}"); + self.lsp_document_versions + .retain(|key, _| !key.starts_with(&prefix)); + let error_key = format!("__lsp__{provider_id}"); + if self.lsp_diagnostics.remove(&error_key).is_some() { + self.sync_diagnostics_panel(cx); + } + } + + /// 合并一轮诊断:空列表代表服务端已清空该文件的问题。 + fn lsp_apply_diagnostics( + &mut self, + updates: HashMap>, + cx: &mut Context, + ) { + let mut changed = false; + for (path, entries) in updates { + if entries.is_empty() { + changed |= self.lsp_diagnostics.remove(&path).is_some(); + } else { + self.lsp_diagnostics.insert(path, entries); + changed = true; + } + } + if changed { + self.sync_diagnostics_panel(cx); + } + } + + /// 启动/同步失败时在诊断面板留一条错误记录,便于定位缺失的服务器。 + fn lsp_push_error(&mut self, provider_id: &str, message: &str, cx: &mut Context) { + let entry = DiagnosticEntry { + severity: "error".to_string(), + file_path: provider_id.to_string(), + line: 0, + column: 0, + message: message.to_string(), + source: Some("LSP".to_string()), + code: None, + }; + self.lsp_diagnostics + .insert(format!("__lsp__{provider_id}"), vec![entry]); + self.sync_diagnostics_panel(cx); + } + + /// 把所有文件的诊断按路径/行/列排序后交给底部面板。 + fn sync_diagnostics_panel(&mut self, cx: &mut Context) { + let mut entries: Vec = + self.lsp_diagnostics.values().flatten().cloned().collect(); + entries.sort_by(|left, right| { + left.file_path + .cmp(&right.file_path) + .then(left.line.cmp(&right.line)) + .then(left.column.cmp(&right.column)) + }); + let _ = self + .bottom_panel + .update(cx, |panel, cx| panel.set_diagnostics(entries, cx)); + } + + /// 活动编辑器上执行动作(无活动窗格时忽略)。 fn with_active_editor( &self, @@ -1153,6 +1430,9 @@ impl WorkbenchView { EditorTabEvent::OpenInTerminal { dir } => { this.send_terminal_command(&format!("cd \"{dir}\""), cx); } + EditorTabEvent::DocumentChanged { path, text } => { + this.lsp_on_document_changed(path.clone(), text.clone(), cx); + } }, ); self._subscriptions.push(sub); From 0c96cfc998b687d26619b5fb50d9fb8ae344b576 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 13:16:09 +0800 Subject: [PATCH 55/59] =?UTF-8?q?feat(linux):=20LSP=20=E9=9B=86=E6=88=90?= =?UTF-8?q?=E4=B8=8E=20Run=20=E9=85=8D=E7=BD=AE=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 lsp.rs 模块:支持 rust-analyzer、clangd、pyright、typescript-language-server、gopls、jdtls 等语言服务器 - 新增 run/ 模块(config.rs + process.rs):重构 Run 配置管理,使用 Core runConfig.inspect/generate/resolve 替代本地文件读写 - bottom_panel.rs:引入 ProcessManager 统一管理子进程生命周期,支持 Run/Maven 多进程隔离 - editor.rs:新增 publish_active_document 事件,支持 LSP 文档同步 - settings_dialog.rs:Run 配置切换时自动重载,支持 workspace root 变更时重置配置状态 - view.rs:LSP 会话管理、诊断聚合、编辑器同步等能力接入 - 更新 Java 项目构建与启动边界笔记 --- ...-java-project-build-and-launch-boundary.md | 15 + linux/Cargo.lock | 1 + linux/Cargo.toml | 1 + linux/src/workbench/bottom_panel.rs | 1361 ++++++++--------- linux/src/workbench/editor.rs | 47 +- linux/src/workbench/mod.rs | 1 + linux/src/workbench/run/config.rs | 1105 +++++++++++++ linux/src/workbench/run/mod.rs | 15 + linux/src/workbench/run/process.rs | 1331 ++++++++++++++++ linux/src/workbench/settings_dialog.rs | 294 ++-- linux/src/workbench/view.rs | 76 +- 11 files changed, 3334 insertions(+), 913 deletions(-) create mode 100644 linux/src/workbench/run/config.rs create mode 100644 linux/src/workbench/run/mod.rs create mode 100644 linux/src/workbench/run/process.rs 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..4339455f0 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,13 @@ classpath/module-path;随后 Run 模块只启动一次项目 JDK。Maven 仍 4. Rust Core 接收结构化 `javaLaunch`,生成 `project-jdk` 直启计划; 5. macOS/Windows 宿主按各自路径分隔符拼接参数并启动一个 JVM。 +Linux 第一阶段沿用同一条配置边界:`bottom_panel.rs` 只发 Core 命令和展示 +状态,`workbench/run/config.rs` 持久化 Core 生成结果、解析工具链与启动计划, +`workbench/run/process.rs` 独占 session、子进程组和输出事件。Linux 不读取旧 +`name/type/mainClass` 文档作为主路径,不再用源码正则补入口,也不在计划失败 +后伪造 `mvn compile`。Maven 项目里的 `java.main` 仍因缺少 JDT 返回的 +`javaLaunch` 明确失败,直到 Linux 接入同一 Java 项目准备能力。 + Run 和 Debug 共用同一套 Java 项目准备逻辑。配置中的 Maven 信息仍用于 JDT LS 导入、Profile、settings.xml 和项目模型;Maven 工具窗口、框架 goal、测试与显式 Maven 任务仍走 Maven 启动计划。 @@ -176,6 +183,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 +212,10 @@ 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、工具链映射、 + pre-launch 失败门禁、过期 sequence 与完整进程组停止;测试不依赖本机 + Maven/JDK/网络。 - 共享契约:`shared/contracts/rust-core-api.md` 与 `shared/contracts/application-boundary.md`,跨平台样例为 `shared/fixtures/lsp/java-build-report-v1.json`。 @@ -213,5 +227,6 @@ 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/workbench/bottom_panel.rs`、`linux/src/workbench/run/` - 相关笔记: `.agents/notes/implemented/feature/2026-09-17-standalone-java-compile-then-run.md` diff --git a/linux/Cargo.lock b/linux/Cargo.lock index 0b98ccd21..375ab4c10 100644 --- a/linux/Cargo.lock +++ b/linux/Cargo.lock @@ -3960,6 +3960,7 @@ dependencies = [ "alacritty_terminal", "anyhow", "gpui-kit", + "libc", "lithe-core", "portable-pty", "rfd", diff --git a/linux/Cargo.toml b/linux/Cargo.toml index d14c4ac61..d5365b396 100644 --- a/linux/Cargo.toml +++ b/linux/Cargo.toml @@ -26,3 +26,4 @@ serde_json = "1" tokio = { version = "1", features = ["sync", "macros", "rt"] } anyhow = "1" uuid = { version = "1", features = ["v4"] } +libc = "0.2" diff --git a/linux/src/workbench/bottom_panel.rs b/linux/src/workbench/bottom_panel.rs index 3079d014a..be61351f9 100644 --- a/linux/src/workbench/bottom_panel.rs +++ b/linux/src/workbench/bottom_panel.rs @@ -1,4 +1,4 @@ -use std::sync::{mpsc, Arc, Mutex}; +use std::sync::mpsc; use gpui_kit::assets::IconName; use gpui_kit::component::button::{Button, ButtonVariants as _}; @@ -14,6 +14,13 @@ use gpui_kit::{ use crate::core::CoreClient; 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; /// 运行历史上限。 @@ -36,51 +43,6 @@ pub enum RunProjectState { Failed(String), } -/// Run 配置行:本地 `.lithe/run/configurations.json` 与 -/// `runConfig.generate` 结果的显示子集。 -#[derive(Debug, Clone)] -pub struct RunConfigItem { - /// core 配置 id(本地文件项用配置名代替;`createLaunchPlan` - /// 可能因此找不到而走退化执行)。 - pub id: String, - pub name: String, - /// `mainClass`(java)|`maven`|`npm`。 - pub kind: String, - /// 副标题:主类 / 模块 / 脚本参数。 - pub detail: String, - pub main_class: Option, - /// java 源码路径(generate 结果的 `extensions.java.source`),单文件退化编译用。 - pub source: Option, -} - -/// 后台运行步骤(退化执行用,不经 core 常驻进程宿主)。 -#[derive(Debug, Clone)] -struct RunStep { - program: String, - args: Vec, - cwd: String, -} - -/// 进程 framing 行文案(`run_steps_blocking` 无 `cx`,调用方预取传入)。 -#[derive(Debug, Clone)] -struct StepText { - start_failed: String, - exit_code: String, - exited: String, - finished: String, -} - -impl StepText { - fn load(cx: &gpui_kit::App) -> Self { - Self { - start_failed: crate::i18n::menu_text(cx, "run.startFailed").to_string(), - exit_code: crate::i18n::menu_text(cx, "run.exitCode").to_string(), - exited: crate::i18n::menu_text(cx, "run.exited").to_string(), - finished: crate::i18n::menu_text(cx, "run.finished").to_string(), - } - } -} - /// 上次 Maven 目标参数(头部重跑按钮回放,对齐 Tauri `rerunLastTest` 语义的子集)。 #[derive(Debug, Clone)] struct MavenGoalParams { @@ -118,7 +80,10 @@ pub struct DiagnosticEntry { /// 诊断面板向外发出的定位事件。 #[derive(Debug, Clone)] pub enum BottomPanelEvent { - OpenFile { path: String, line: u32 }, + OpenFile { + path: String, + line: u32, + }, /// 面板清空按钮:请求宿主同步丢弃缓存的 LSP 诊断。 ClearDiagnostics, } @@ -145,14 +110,24 @@ pub struct BottomPanelView { pub(crate) run_state: RunProjectState, /// Run 面板工程展示名(工作区目录名)。 pub(crate) run_project_name: String, - /// Run 配置列表(本地文件优先,否则 generate 结果)。 + /// 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, /// 运行输出滚动句柄(跟随末尾用)。 @@ -167,16 +142,18 @@ pub struct BottomPanelView { pub(crate) maven_running: bool, /// core 客户端(`reload_run_project` / `createLaunchPlan` 经它走 core JSON 命令)。 client: CoreClient, - /// 在跑子进程句柄:停止按钮经 `kill` 停,运行线程经 `try_wait` 短锁轮询收割。 - run_child: Arc>>, - /// Maven 任务子进程句柄(与 Run 共用停止语义,分开存放可各自启停)。 - maven_child: Arc>>, + /// 所有 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 记录加载失败时的展示文案;成功后清空。 @@ -200,21 +177,27 @@ impl BottomPanelView { 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, - maven_child: Arc::new(Mutex::new(None)), + client: CoreClient::new(), + processes: ProcessManager::new(), maven_seq: 0, last_maven_goal: None, - client: CoreClient::new(), - run_child: Arc::new(Mutex::new(None)), run_seq: 0, + run_execution_seq: 0, + run_editor: None, git_log: Vec::new(), git_log_error: None, } @@ -360,85 +343,270 @@ impl BottomPanelView { /// 更新工作目录并重探 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 工程:本地 `.lithe/run/configurations.json` 优先命中即 `Ready`; - /// 否则先 `maven.scan`(payload `{root, paths}`)探测工程类型,再 - /// `runConfig.generate`(payload `{root}`,返回 - /// `{generated: {configurations[]}, entryCount}`)取配置列表,失败即 `Failed`。 + /// 设置 Run 前需要保存的编辑器实体。 + pub fn set_run_editor(&mut self, editor: Option>, cx: &mut Context) { + self.run_editor = editor; + 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; - self.run_project_name = crate::settings::project_dir_name(&self.working_dir).to_string(); - let local = read_local_run_configs(&self.working_dir); - if !local.is_empty() { - self.run_configs = local; - self.keep_run_selection(); - self.run_state = RunProjectState::Ready; - cx.notify(); - return; + 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(); cx.spawn(async move |this, cx| { - // 先探 Maven 工程(只作工程类型参考;失败不直接 Failed,交给 generate 定夺)。 - let _ = client + let inspection = client .execute::( &cx, - "maven.scan", - serde_json::json!({ "root": root, "paths": [] }), + "runConfig.inspect", + serde_json::json!({ "root": root, "checkFingerprint": true }), ) .await; - let generated = client + 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 generated = client + .execute::( + &cx, + "runConfig.generate", + serde_json::json!({ + "root": root, + "paths": paths, + "modulePaths": [] + }), + ) + .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.generate", - serde_json::json!({ "root": root }), + "runConfig.resolve", + serde_json::json!({ + "root": root, + "toolchainCandidates": candidates + }), ) .await; - let _ = this.update(cx, |view, cx| { - if view.run_seq != seq { + 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; } - match generated { - Ok(value) => { - view.run_configs = parse_generated_configs(&value); - // 无 LSP 时 generate 为空:本地文本扫描 main 方法兜底 - //(对齐 Tauri 入口点发现,Linux 无 JDT 故用源码文本匹配)。 - if view.run_configs.is_empty() { - view.run_configs = scan_java_mains(&root); + }; + 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(); } - view.keep_run_selection(); - view.run_state = if view.run_configs.is_empty() { - RunProjectState::Missing - } else { - RunProjectState::Ready - }; - } - Err(err) => { - view.run_state = RunProjectState::Failed(err); - } + }); + 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; } - let Some(item) = self.selected_run_item() else { + 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!( @@ -448,122 +616,256 @@ impl BottomPanelView { ), ); cx.notify(); - let (tx, rx) = mpsc::channel::(); - let child_slot = self.run_child.clone(); - let text = StepText::load(cx); + 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 processes = self.processes.clone(); cx.spawn(async move |this, cx| { - // 先问 core 要可执行计划(payload `{root, configurationId}`,返回 - // `{executable, arguments, workingDirectory}`);本地配置或工具链 - // 未解析时拿不到可直跑的命令,退化到按 kind 拼的本地命令。 + 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 payload = create_launch_plan_request( + &root, + &item, + current_file.as_deref(), + maven_context.as_ref(), + None, + ); let plan = client .execute::( &cx, "runConfig.createLaunchPlan", - serde_json::json!({ "root": root, "configurationId": item.id }), + payload, ) - .await - .ok(); - let steps = plan_to_steps(plan.as_ref(), &item, &root); - let started = this.update(cx, |view, cx| { - if steps.is_empty() { - view.run_running = false; - push_run_line( - &mut view.run_output, - crate::i18n::menu_text(cx, "run.noSteps").to_string(), - ); - cx.notify(); - return false; + .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 display = format!("$ {} {}", steps[0].program, steps[0].args.join(" ")); - push_run_line(&mut view.run_output, display.clone()); - view.record_run(&display, cx); - cx.notify(); - true - }); - if !matches!(started, Ok(true)) { + }; + 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; } - std::thread::spawn(move || run_steps_blocking(steps, child_slot, text, tx)); - // 输出泵:每行经 background 线程阻塞收,再回到主线程落盘展示。 - let rx = Arc::new(Mutex::new(rx)); + 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 = rx.clone(); - let next = cx + let slot = receiver.clone(); + let event = cx .background_executor() .spawn(async move { let guard = slot.lock().ok()?; - guard.recv().ok() + Some( + match guard.recv_timeout(std::time::Duration::from_secs(1)) { + Ok(event) => Ok(event), + Err(error) => Err(error), + }, + ) }) .await; - match next { - Some(line) => { - if this - .update(cx, |view, cx| { - push_run_line(&mut view.run_output, line); - cx.notify(); - }) - .is_err() - { - break; + 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(); } - None => break, + }); + if finished { + break; } } - let _ = this.update(cx, |view, cx| { - view.run_running = false; - cx.notify(); - }); + let _ = handle.join(); }) .detach(); } - /// 停止在跑进程(Run 与 Maven 各自 kill;mvn 拉起的 java 孙进程不在此列)。 + /// 停止在跑进程;ProcessManager 异步终止完整进程树并在输出泵中回收。 pub fn stop_running(&mut self, cx: &mut Context) { let mut stopped = false; - if let Ok(mut slot) = self.run_child.lock() { - if let Some(child) = slot.as_mut() { - let _ = child.kill(); - stopped = true; - } - } - if let Ok(mut slot) = self.maven_child.lock() { - if let Some(child) = slot.as_mut() { - let _ = child.kill(); - stopped = true; - } - } 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 目标:切 Maven 页 + 经 core `maven.launchPlan` 拿确定性参数 - ///(payload `{root, context: {version: 1, reactorPath: ".", profiles, skipTests}, module, goals}`), - /// 起受管进程并把输出泵入 Maven 页(首行 `$ mvn …`,对齐 Tauri Maven 页)。 - /// 已有任务在跑时先停掉(对齐 Tauri 停掉上一个 session)。 + /// 运行 Maven 目标:Core 生成计划,Linux 只解析工具链并托管进程。 pub fn run_maven_goal( &mut self, pom_path: &str, @@ -578,11 +880,7 @@ impl BottomPanelView { return; } if self.maven_running { - if let Ok(mut slot) = self.maven_child.lock() { - if let Some(child) = slot.as_mut() { - let _ = child.kill(); - } - } + let _ = self.processes.request_stop("maven", None); } self.maven_seq += 1; let seq = self.maven_seq; @@ -596,7 +894,7 @@ impl BottomPanelView { }); self.active_tab = BottomTab::Maven; self.is_collapsed = false; - self.maven_title = Some(title.clone()); + self.maven_title = Some(title); self.maven_output.clear(); self.maven_running = true; cx.notify(); @@ -605,8 +903,7 @@ impl BottomPanelView { let root = self.working_dir.clone(); let module = maven_module_for_pom(&self.working_dir, pom_path); let profiles = profiles.to_vec(); - let text = StepText::load(cx); - let child_slot = self.maven_child.clone(); + let processes = self.processes.clone(); cx.spawn(async move |this, cx| { let plan = client .execute::( @@ -624,63 +921,141 @@ impl BottomPanelView { "goals": [goal], }), ) - .await - .ok(); - let (program, args, cwd) = maven_plan_to_step(plan.as_ref(), &root, &goal); - let display = format!("$ {} {}", program, args.join(" ")); - let started = this.update(cx, |view, cx| { - if view.maven_seq != seq { - return false; + .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; } - push_run_line(&mut view.maven_output, display.clone()); - view.record_run(&display, cx); - cx.notify(); - true - }); - if !matches!(started, Ok(true)) { + }; + 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 steps = vec![RunStep { program, args, cwd }]; - let (tx, rx) = mpsc::channel::(); - std::thread::spawn(move || run_steps_blocking(steps, child_slot, text, tx)); - let rx = Arc::new(Mutex::new(rx)); + 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 = rx.clone(); - let next = cx + let slot = receiver.clone(); + let event = cx .background_executor() .spawn(async move { let guard = slot.lock().ok()?; - guard.recv().ok() + Some( + match guard.recv_timeout(std::time::Duration::from_secs(1)) { + Ok(event) => Ok(event), + Err(error) => Err(error), + }, + ) }) .await; - match next { - Some(line) => { - // `run_steps_blocking` 首行会再发一次 `$ …`,Maven 页已有 - // 展示首行,跳过重复(对齐 Tauri 单首行)。 - let skip = this - .update(cx, |view, cx| { - let duplicate = line.trim_start().starts_with("$ ") - && !view.maven_output.is_empty(); - if !duplicate { - push_run_line(&mut view.maven_output, line); - } - cx.notify(); - view.maven_seq == seq - }) - .unwrap_or(false); - if !skip { - break; + 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; } } - None => break, + cx.notify(); + }); + if finished { + break; } } - let _ = this.update(cx, |view, cx| { - if view.maven_seq == seq { - view.maven_running = false; - } - cx.notify(); - }); + let _ = handle.join(); }) .detach(); } @@ -703,23 +1078,16 @@ impl BottomPanelView { } } - /// 当前选中项(无选中或已失效时回退首项)。 + 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(|c| c.id == id).cloned()) - .or_else(|| self.run_configs.first().cloned()) - } - - /// 保持选中有效:原选中仍在则不动,否则选首项。 - fn keep_run_selection(&mut self) { - let keep = self - .selected_run_config - .as_ref() - .is_some_and(|id| self.run_configs.iter().any(|c| &c.id == id)); - if !keep { - self.selected_run_config = self.run_configs.first().map(|c| c.id.clone()); - } + .and_then(|id| self.run_configs.iter().find(|item| item.id == id).cloned()) } /// 重新加载 Git 提交记录。core 的 `git.historyPage` 需经宿主异步接线, @@ -998,8 +1366,6 @@ impl BottomPanelView { .primary() .label(run_ui_text(cx, "生成配置", "Generate")) .on_click(cx.listener(|this, _event, _window, cx| { - let root = this.working_dir.clone(); - let _ = append_default_run_config(&root); this.reload_run_project(cx); })), ) @@ -1008,6 +1374,19 @@ impl BottomPanelView { } } + 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() @@ -1054,8 +1433,7 @@ impl BottomPanelView { .icon(IconName::Play) .tooltip(run_ui_text(cx, "运行", "Run")) .on_click(cx.listener(move |this, _event, _window, cx| { - this.selected_run_config = Some(run_id.clone()); - this.run_selected_config(cx); + this.run_configuration_id(run_id.clone(), cx); })), ) .on_click(cx.listener(move |this, _event, _window, cx| { @@ -1103,7 +1481,8 @@ impl BottomPanelView { .border_r_1() .border_color(ThemeColors::border()) .py_1() - .children(rows), + .children(rows) + .children(diagnostic_rows), ) .child( div() @@ -1346,297 +1725,23 @@ fn load_git_log(workdir: &str) -> Result, String> { Ok(entries) } -/// 读本地运行配置(`/.lithe/run/configurations.json`),兼容数组与 -/// `{configurations: []}` 两种形状;字段沿用 v1 形态(`name/type/mainClass`)。 -fn read_local_run_configs(workspace_root: &str) -> Vec { - let path = std::path::Path::new(workspace_root) - .join(".lithe") - .join("run") - .join("configurations.json"); - let Ok(text) = std::fs::read_to_string(&path) else { - return Vec::new(); - }; - let Ok(value) = serde_json::from_str::(&text) else { - return Vec::new(); - }; - let arr = if let Some(arr) = value.as_array() { - arr.clone() - } else if let Some(arr) = value.get("configurations").and_then(|v| v.as_array()) { - arr.clone() +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 { - return Vec::new(); + path }; - arr.iter() - .filter_map(|item| { - let name = item.get("name")?.as_str()?; - let kind_raw = item.get("type").and_then(|t| t.as_str()).unwrap_or("java"); - let main_class = item - .get("mainClass") - .and_then(|m| m.as_str()) - .map(str::to_string); - let kind = if kind_raw.contains("npm") || kind_raw.contains("node") { - "npm" - } else if kind_raw.contains("maven") { - "maven" - } else { - "mainClass" - }; - let detail = main_class.clone().unwrap_or_else(|| kind_raw.to_string()); - Some(RunConfigItem { - id: name.to_string(), - name: name.to_string(), - kind: kind.to_string(), - detail, - main_class, - source: None, - }) - }) - .collect() -} - -/// 追加一个默认 Java 运行配置(保留已有配置),供 Missing 空态的生成按钮用。 -fn append_default_run_config(workspace_root: &str) -> bool { - let path = std::path::Path::new(workspace_root) - .join(".lithe") - .join("run") - .join("configurations.json"); - if let Some(parent) = path.parent() { - if std::fs::create_dir_all(parent).is_err() { - return false; - } - } - let mut list: Vec = std::fs::read_to_string(&path) - .ok() - .and_then(|text| serde_json::from_str::(&text).ok()) - .and_then(|v| { - if let Some(arr) = v.as_array() { - Some(arr.clone()) - } else { - v.get("configurations").and_then(|c| c.as_array()).cloned() - } - }) - .unwrap_or_default(); - list.push(serde_json::json!({"name": "Run Main", "type": "java", "mainClass": "Main"})); - let value = serde_json::json!({"configurations": list}); - serde_json::to_string_pretty(&value) - .ok() - .and_then(|text| std::fs::write(&path, text).ok()) - .is_some() + Some(relative.to_string_lossy().replace('\\', "/")) } -/// 解析 `runConfig.generate` 返回的 `{generated: {configurations[]}}`。 -/// 本地 Java 入口扫描:无 LSP 时的兜底(对齐 Tauri `discoverJavaEntrypoints`, -/// Linux 无 JDT 故用源码文本匹配 `public static void main`)。 -/// 主类由 `package` 声明 + 文件名推导;上限 20 个,按路径排序保证稳定。 -fn scan_java_mains(root: &str) -> Vec { - const MAX_MAINS: usize = 20; - const MAX_DEPTH: usize = 8; - const SKIP_DIRS: &[&str] = &[ - "target", - "build", - "out", - "dist", - "node_modules", - ".git", - ".idea", - ".vscode", - "vendor", - ]; - const MAX_FILE_BYTES: u64 = 512 * 1024; - - fn walk(dir: &std::path::Path, depth: usize, out: &mut Vec) { - if depth > MAX_DEPTH || out.len() >= MAX_MAINS * 4 { - return; - } - let Ok(entries) = std::fs::read_dir(dir) else { - return; - }; - let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect(); - entries.sort_by_key(|e| e.file_name()); - for entry in entries { - let path = entry.path(); - let name = entry.file_name().to_string_lossy().to_string(); - if path.is_dir() { - if !name.starts_with('.') && !SKIP_DIRS.contains(&name.as_str()) { - walk(&path, depth + 1, out); - } - } else if name.ends_with(".java") { - out.push(path); - } - } - } - - fn package_of(text: &str) -> Option { - text.lines().find_map(|line| { - let line = line.trim(); - line.strip_prefix("package ") - .and_then(|rest| rest.strip_suffix(';')) - .map(|pkg| pkg.trim().to_string()) - .filter(|pkg| { - !pkg.is_empty() - && pkg - .chars() - .all(|c| c.is_alphanumeric() || c == '.' || c == '_') - }) - }) - } - - let root_path = std::path::Path::new(root); - let mut files = Vec::new(); - walk(root_path, 0, &mut files); - let mut items = Vec::new(); - for path in files { - if items.len() >= MAX_MAINS { - break; - } - if std::fs::metadata(&path).is_ok_and(|m| m.len() > MAX_FILE_BYTES) { - continue; - } - let Ok(text) = std::fs::read_to_string(&path) else { - continue; - }; - if !text.contains("public static void main") { - continue; - } - let stem = path - .file_stem() - .map(|s| s.to_string_lossy().to_string()) - .unwrap_or_default(); - if stem.is_empty() { - continue; - } - let main_class = match package_of(&text) { - Some(pkg) => format!("{pkg}.{stem}"), - None => stem, - }; - let source = path - .strip_prefix(root_path) - .map(|p| p.to_string_lossy().replace('\\', "/")) - .unwrap_or_default(); - items.push(RunConfigItem { - id: format!("local-main:{main_class}"), - name: main_class.clone(), - kind: "mainClass".to_string(), - detail: main_class.clone(), - main_class: Some(main_class), - source: Some(source), - }); - } - items.sort_by(|a, b| a.name.cmp(&b.name)); - items -} - -fn parse_generated_configs(value: &serde_json::Value) -> Vec { - value - .get("generated") - .and_then(|g| g.get("configurations")) - .and_then(|c| c.as_array()) - .map(|arr| arr.iter().filter_map(config_item_from_generated).collect()) - .unwrap_or_default() -} - -fn config_item_from_generated(item: &serde_json::Value) -> Option { - let id = item.get("id")?.as_str()?; - let name = item.get("name").and_then(|n| n.as_str()).unwrap_or(id); - let provider = item.get("provider").and_then(|p| p.as_str()).unwrap_or(""); - let command = item.get("command").and_then(|c| c.as_str()).unwrap_or(""); - let maven = &item["extensions"]["maven"]; - let main_class = maven - .get("mainClass") - .and_then(|m| m.as_str()) - .map(str::to_string); - let module = maven - .get("module") - .and_then(|m| m.as_str()) - .map(str::to_string); - let source = item - .get("extensions") - .and_then(|e| e.get("java")) - .and_then(|j| j.get("source")) - .and_then(|s| s.as_str()) - .map(str::to_string); - let kind = if provider.contains("npm") || provider.contains("node") || command == "npm" { - "npm" - } else if provider.contains("maven") { - "maven" - } else { - "mainClass" - }; - let detail = if kind == "npm" { - item.get("args") - .and_then(|a| a.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str()) - .collect::>() - .join(" ") - }) - .filter(|s| !s.is_empty()) - .unwrap_or_else(|| "run dev".to_string()) - } else { - main_class - .clone() - .or(module.clone()) - .unwrap_or_else(|| provider.to_string()) - }; - Some(RunConfigItem { - id: id.to_string(), - name: name.to_string(), - kind: kind.to_string(), - detail, - main_class, - source, - }) -} - -/// 由 `createLaunchPlan` 结果拼直跑步骤:只有 `executable.command` -/// 是真实可执行路径;`executable.toolchain` 需宿主解析,拿不到就走退化。 -fn plan_to_steps( - plan: Option<&serde_json::Value>, - item: &RunConfigItem, - root: &str, -) -> Vec { - if let Some(plan) = plan { - let cwd = plan - .get("workingDirectory") - .and_then(|v| v.as_str()) - .unwrap_or("."); - let cwd = if cwd == "." || cwd.is_empty() { - root.to_string() - } else { - format!("{root}/{cwd}") - }; - if let Some(command) = plan - .get("executable") - .and_then(|e| e.get("command")) - .and_then(|c| c.as_str()) - .filter(|c| !c.is_empty()) - { - let args = plan - .get("arguments") - .and_then(|a| a.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str().map(str::to_string)) - .collect() - }) - .unwrap_or_default(); - return vec![RunStep { - program: command.to_string(), - args, - cwd, - }]; - } - } - fallback_run_steps(item, root) -} - -/// pom 所在目录相对 root 的模块路径:根 pom 为 `.`,与 core 模块约定一致。 +/// pom 所在目录相对 root 的模块路径:根 pom 为 `.`。 fn maven_module_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('\\', "/")) + .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() { @@ -1646,228 +1751,6 @@ fn maven_module_for_pom(root: &str, pom: &str) -> String { } } -/// `maven.launchPlan`(`{executable.toolchain, arguments[], workingDirectory}`) -/// 转本地执行步骤;拿不到计划时退化为 `mvn `。 -fn maven_plan_to_step( - plan: Option<&serde_json::Value>, - root: &str, - goal: &str, -) -> (String, Vec, String) { - if let Some(plan) = plan { - let cwd = plan - .get("workingDirectory") - .and_then(|v| v.as_str()) - .unwrap_or("."); - let cwd = if cwd == "." || cwd.is_empty() { - root.to_string() - } else { - format!("{root}/{cwd}") - }; - let args = plan - .get("arguments") - .and_then(|a| a.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str().map(str::to_string)) - .collect::>() - }) - .filter(|args| !args.is_empty()); - if let Some(args) = args { - return (resolve_maven_executable(root), args, cwd); - } - } - ( - resolve_maven_executable(root), - vec![goal.to_string()], - root.to_string(), - ) -} - -/// 退化执行规则:maven 用 mvn 跑 `compile exec:java`;npm 跑 `run dev`; -/// java 单文件走 javac+java,否则直跑主类。 -fn fallback_run_steps(item: &RunConfigItem, root: &str) -> Vec { - match item.kind.as_str() { - "maven" => { - let mut args = vec!["-B".to_string(), "-ntp".to_string(), "compile".to_string()]; - if let Some(main) = item.main_class.as_deref().filter(|s| !s.is_empty()) { - args.push("exec:java".to_string()); - args.push(format!("-Dexec.mainClass={main}")); - } - vec![RunStep { - program: resolve_maven_executable(root), - args, - cwd: root.to_string(), - }] - } - "npm" => vec![RunStep { - program: "npm".to_string(), - args: vec!["run".to_string(), pick_npm_script(root)], - cwd: root.to_string(), - }], - _ => { - let classes = format!("{root}/.lithe/run/classes"); - if let (Some(source), Some(main)) = (item.source.as_deref(), item.main_class.as_deref()) - { - let abs = format!("{root}/{source}"); - if !source.is_empty() && !main.is_empty() && std::path::Path::new(&abs).is_file() { - let _ = std::fs::create_dir_all(&classes); - return vec![ - RunStep { - program: "javac".to_string(), - args: vec!["-d".to_string(), classes.clone(), abs], - cwd: root.to_string(), - }, - RunStep { - program: "java".to_string(), - args: vec!["-cp".to_string(), classes, main.to_string()], - cwd: root.to_string(), - }, - ]; - } - } - let main = item.main_class.clone().unwrap_or_else(|| item.name.clone()); - vec![RunStep { - program: "java".to_string(), - args: vec![main], - cwd: root.to_string(), - }] - } - } -} - -/// mvn 路径:先看 `.lithe/run/local.json` 的 `mavenExecutablePath` -///(含 `toolchain` 下),没有就用 `PATH` 里的 `mvn`。 -fn resolve_maven_executable(root: &str) -> String { - let path = std::path::Path::new(root) - .join(".lithe") - .join("run") - .join("local.json"); - let from_file = std::fs::read_to_string(&path) - .ok() - .and_then(|text| serde_json::from_str::(&text).ok()) - .and_then(|value| { - value - .get("mavenExecutablePath") - .or_else(|| { - value - .get("toolchain") - .and_then(|t| t.get("mavenExecutablePath")) - }) - .and_then(|v| v.as_str()) - .map(str::to_string) - }) - .filter(|s| !s.trim().is_empty()); - from_file.unwrap_or_else(|| "mvn".to_string()) -} - -/// npm 脚本选择:`package.json` 有 dev 用 dev,否则 start,再没有还用 dev(报错由输出展示)。 -fn pick_npm_script(root: &str) -> String { - let has = |name: &str, value: &serde_json::Value| { - value.get("scripts").and_then(|s| s.get(name)).is_some() - }; - std::fs::read_to_string(std::path::Path::new(root).join("package.json")) - .ok() - .and_then(|text| serde_json::from_str::(&text).ok()) - .map(|value| { - if has("dev", &value) { - "dev".to_string() - } else if has("start", &value) { - "start".to_string() - } else { - "dev".to_string() - } - }) - .unwrap_or_else(|| "dev".to_string()) -} - -/// 顺序跑步骤并把输出行推入 channel;首个失败步骤后停。stdout/stderr 各一根 -/// 转发线程;子进程句柄共享给停止按钮,`try_wait` 短锁轮询避免 `wait` -/// 占锁导致 kill 拿不到锁。 -fn run_steps_blocking( - steps: Vec, - child_slot: Arc>>, - text: StepText, - tx: mpsc::Sender, -) { - use std::io::BufRead as _; - for step in &steps { - let _ = tx.send(format!("$ {} {}", step.program, step.args.join(" "))); - let mut child = match std::process::Command::new(&step.program) - .args(&step.args) - .current_dir(&step.cwd) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .spawn() - { - Ok(child) => child, - Err(err) => { - let _ = tx.send(format!("{}:{err}", text.start_failed)); - return; - } - }; - let stdout = child.stdout.take(); - let stderr = child.stderr.take(); - if let Ok(mut slot) = child_slot.lock() { - *slot = Some(child); - } - let tx_out = tx.clone(); - let out_handle = stdout.map(|out| { - std::thread::spawn(move || { - for line in std::io::BufReader::new(out).lines().map_while(Result::ok) { - if tx_out.send(line).is_err() { - break; - } - } - }) - }); - let tx_err = tx.clone(); - let err_handle = stderr.map(|err| { - std::thread::spawn(move || { - for line in std::io::BufReader::new(err).lines().map_while(Result::ok) { - if tx_err.send(line).is_err() { - break; - } - } - }) - }); - let status = loop { - let Ok(mut slot) = child_slot.lock() else { - break None; - }; - match slot.as_mut().map(|child| child.try_wait()) { - Some(Ok(Some(status))) => break Some(status), - Some(Ok(None)) => { - drop(slot); - std::thread::sleep(std::time::Duration::from_millis(50)); - } - _ => break None, - } - }; - if let Some(handle) = out_handle { - let _ = handle.join(); - } - if let Some(handle) = err_handle { - let _ = handle.join(); - } - if let Ok(mut slot) = child_slot.lock() { - let _ = slot.take(); - } - match status { - Some(status) if status.success() => { - let _ = tx.send(format!("{}:0", text.exit_code)); - } - Some(status) => { - let _ = tx.send(format!("{}:{status}", text.exited)); - return; - } - None => { - let _ = tx.send(text.finished.clone()); - return; - } - } - } -} - /// 行首空白转不换行空格:GPUI `Normal` 会塌缩连续空白(Tauri `pre-wrap` /// 保留),只转行首以兼顾缩进保留与行内换行。 fn preserve_leading_whitespace(line: &str) -> String { diff --git a/linux/src/workbench/editor.rs b/linux/src/workbench/editor.rs index 10e5fbbdf..d40a745b4 100644 --- a/linux/src/workbench/editor.rs +++ b/linux/src/workbench/editor.rs @@ -247,38 +247,51 @@ impl EditorView { cx.notify(); } - /// 保存当前活动的标签页至磁盘(对接 `file.write`) + /// 返回当前活动文件路径,供 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; + return cx.spawn(async move |_this, _cx| Ok(())); }; let Some(tab) = self.tabs.get(idx) else { - return; + 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| { - let task = client.write_file(&cx, &root, &path, &text); - if task.await.is_ok() { - let _ = this.update(cx, |ed, cx| { - if let Some(active_idx) = ed.active_tab_index { - if let Some(t) = ed.tabs.get_mut(active_idx) { - if t.path == path { - t.is_dirty = false; - cx.notify(); - } + 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(()) }) - .detach(); } /// 全文替换并记入撤销历史(`set_value` 不触发 Change,需手动维护历史与标签同步)。 diff --git a/linux/src/workbench/mod.rs b/linux/src/workbench/mod.rs index 3ae503ed6..9d870daef 100644 --- a/linux/src/workbench/mod.rs +++ b/linux/src/workbench/mod.rs @@ -11,6 +11,7 @@ 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; 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/settings_dialog.rs b/linux/src/workbench/settings_dialog.rs index 8342ceac5..08edadfaa 100644 --- a/linux/src/workbench/settings_dialog.rs +++ b/linux/src/workbench/settings_dialog.rs @@ -20,8 +20,13 @@ use gpui_kit::{ 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)] @@ -134,11 +139,12 @@ impl SettingsCategory { } } -/// 对话框对外事件:关闭,以及设置已变更(宿主可据此同步其它视图)。 +/// 对话框对外事件:关闭、普通设置变更,以及 Run 文档/工具链变更。 #[derive(Debug, Clone)] pub enum SettingsEvent { Close, Changed, + RunConfigurationChanged, } /// 居中设置面板(宽 820px,高 620px)。 @@ -164,6 +170,8 @@ pub struct SettingsDialog { project_status: String, run_configs: Vec, run_status: String, + run_load_seq: u64, + client: CoreClient, /// AI 模型单行输入(provider 切换不重置,由用户显式修改)。 ai_model_input: Option>, /// Git 可执行路径单行输入。 @@ -199,6 +207,8 @@ impl SettingsDialog { 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, @@ -207,19 +217,134 @@ impl SettingsDialog { } } + /// 切换工作区并使旧的 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(); @@ -233,6 +358,7 @@ impl SettingsDialog { 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; @@ -1152,7 +1278,7 @@ impl SettingsDialog { fill( &mut self.project_jdk_input, if jdk.is_empty() { - which("java").unwrap_or_default() + which_java_home().unwrap_or_default() } else { jdk }, @@ -1284,6 +1410,9 @@ impl SettingsDialog { "保存失败".to_string() }; cx.emit(SettingsEvent::Changed); + if ok { + cx.emit(SettingsEvent::RunConfigurationChanged); + } cx.notify(); })), ), @@ -1292,13 +1421,8 @@ impl SettingsDialog { ) } - /// 运行配置:读写 `/.lithe/run/configurations.json` - ///(对齐 Mac `MacRunConfigurationStore` 的项目级配置),列表展示已有 - /// 配置,生成按钮追加默认 Java 配置。 + /// 运行配置:通过 Core inspect/resolve 读取,生成按钮走 Core generate。 fn render_run_content(&mut self, cx: &mut Context) -> impl IntoElement { - if self.run_configs.is_empty() && self.run_status.is_empty() { - self.run_configs = read_run_config_names(&self.workspace_root); - } let configs = self.run_configs.clone(); let status = self.run_status.clone(); let list = if configs.is_empty() { @@ -1354,16 +1478,7 @@ impl SettingsDialog { .to_string(), ) .on_click(cx.listener(|this, _event, _window, cx| { - let root = this.workspace_root.clone(); - let ok = append_default_run_config(&root); - this.run_configs = read_run_config_names(&root); - this.run_status = if ok { - "已生成默认配置".to_string() - } else { - "生成失败".to_string() - }; - cx.emit(SettingsEvent::Changed); - cx.notify(); + this.refresh_run_configs(true, cx); })), ), ), @@ -3033,6 +3148,15 @@ fn which(exe: &str) -> Option { None } +fn which_java_home() -> Option { + let executable = which("java")?; + let path = std::fs::canonicalize(&executable) + .unwrap_or_else(|_| std::path::PathBuf::from(&executable)); + path.parent() + .and_then(std::path::Path::parent) + .map(|home| home.to_string_lossy().into_owned()) +} + /// Linux 可用 shell 探测:仅收录真实存在的系统 shell(对齐 Tauri 终端面板 /// 的“探测 shells”行为,Windows 的 powershell/cmd/wsl 在 Linux 无意义)。 fn detect_linux_shells() -> Vec { @@ -3048,128 +3172,26 @@ fn detect_linux_shells() -> Vec { shells } -/// 项目 local.json 路径(`/.lithe/run/local.json`,对齐 Mac -/// `MacRunConfigurationStore` 的本机配置位置)。 -fn project_local_json(workspace_root: &str) -> std::path::PathBuf { - std::path::Path::new(workspace_root) - .join(".lithe") - .join("run") - .join("local.json") -} - -/// 读项目工具链(javaHome/mavenExecutable/mavenJavaHome 三键,缺失即空)。 +/// 读项目工具链;canonical 键优先,兼容历史 Linux 扁平键。 fn read_local_toolchain(workspace_root: &str) -> (String, String, String) { - let Ok(text) = std::fs::read_to_string(project_local_json(workspace_root)) else { - return (String::new(), String::new(), String::new()); - }; - let Ok(value) = serde_json::from_str::(&text) else { - return (String::new(), String::new(), String::new()); - }; - let toolchain = value.get("toolchain"); - let get = |key: &str| { - toolchain - .and_then(|t| t.get(key)) - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string() - }; + let paths = read_toolchain_paths(workspace_root); ( - get("javaHome"), - get("mavenExecutable"), - get("mavenJavaHome"), + paths.java_home_path, + paths.maven_executable_path, + paths.maven_java_home_path, ) } -/// 写项目工具链(JSON merge,保留 local.json 其余键)。 +/// 写项目工具链;使用 canonical 键并保留 local.json 其它配置。 fn write_local_toolchain(workspace_root: &str, jdk: &str, maven: &str, maven_jdk: &str) -> bool { - let path = project_local_json(workspace_root); - if let Some(parent) = path.parent() { - if std::fs::create_dir_all(parent).is_err() { - return false; - } - } - let mut value: serde_json::Value = std::fs::read_to_string(&path) - .ok() - .and_then(|text| serde_json::from_str(&text).ok()) - .unwrap_or_else(|| serde_json::json!({})); - if !value.is_object() { - value = serde_json::json!({}); - } - let toolchain = value - .as_object_mut() - .expect("object checked") - .entry("toolchain") - .or_insert_with(|| serde_json::json!({})); - if let Some(map) = toolchain.as_object_mut() { - map.insert("javaHome".to_string(), serde_json::json!(jdk)); - map.insert("mavenExecutable".to_string(), serde_json::json!(maven)); - map.insert("mavenJavaHome".to_string(), serde_json::json!(maven_jdk)); - } - serde_json::to_string_pretty(&value) - .ok() - .and_then(|text| std::fs::write(&path, text).ok()) - .is_some() -} - -/// 运行配置文件路径(`/.lithe/run/configurations.json`)。 -fn run_configurations_path(workspace_root: &str) -> std::path::PathBuf { - std::path::Path::new(workspace_root) - .join(".lithe") - .join("run") - .join("configurations.json") -} - -/// 读运行配置名列表(兼容数组与 `{configurations:[]}` 两种形状)。 -fn read_run_config_names(workspace_root: &str) -> Vec { - let Ok(text) = std::fs::read_to_string(run_configurations_path(workspace_root)) else { - return Vec::new(); - }; - let Ok(value) = serde_json::from_str::(&text) else { - return Vec::new(); - }; - let list = if let Some(arr) = value.as_array() { - arr.clone() - } else if let Some(arr) = value.get("configurations").and_then(|v| v.as_array()) { - arr.clone() - } else { - return Vec::new(); - }; - list.iter() - .filter_map(|item| { - let name = item.get("name").and_then(|n| n.as_str()).unwrap_or("?"); - let kind = item.get("type").and_then(|t| t.as_str()).unwrap_or(""); - if kind.is_empty() { - Some(name.to_string()) - } else { - Some(format!("{name} ({kind})")) - } - }) - .collect() -} - -/// 追加一个默认 Java 运行配置(保留已有配置)。 -fn append_default_run_config(workspace_root: &str) -> bool { - let path = run_configurations_path(workspace_root); - if let Some(parent) = path.parent() { - if std::fs::create_dir_all(parent).is_err() { - return false; - } - } - let mut list: Vec = std::fs::read_to_string(&path) - .ok() - .and_then(|text| serde_json::from_str::(&text).ok()) - .and_then(|v| { - if let Some(arr) = v.as_array() { - Some(arr.clone()) - } else { - v.get("configurations").and_then(|c| c.as_array()).cloned() - } - }) - .unwrap_or_default(); - list.push(serde_json::json!({"name": "Run Main", "type": "java", "mainClass": "Main"})); - let value = serde_json::json!({"configurations": list}); - serde_json::to_string_pretty(&value) - .ok() - .and_then(|text| std::fs::write(&path, text).ok()) - .is_some() + write_toolchain_paths( + workspace_root, + &ToolchainPaths { + java_home_path: jdk.to_string(), + maven_executable_path: maven.to_string(), + maven_java_home_path: maven_jdk.to_string(), + ..ToolchainPaths::default() + }, + ) + .is_ok() } diff --git a/linux/src/workbench/view.rs b/linux/src/workbench/view.rs index 6d00bcdc0..bcbf39db8 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -3,6 +3,7 @@ //! 结构对齐 Tauri `MainLayout`:顶栏 / 多项目标签条 / 活动栏 + 侧边栏 + 编辑区 + //! 右侧插件活动栏 / 底部面板 / 状态栏;无项目时显示欢迎页,浮层由模态状态控制。 +use gpui_kit::assets::IconName; use gpui_kit::component::button::{Button, ButtonVariants as _}; use gpui_kit::component::resizable::{h_resizable, resizable_panel, v_resizable, ResizableState}; use gpui_kit::component::{h_flex, v_flex, Sizable as _}; @@ -180,12 +181,18 @@ impl WorkbenchView { pane_editors.insert(initial_pane, editor.clone()); Self::wire_pane_editor(&pane_tree, cx.entity(), initial_pane, &editor, cx); let bottom_panel = cx.new(|cx| BottomPanelView::new(root.clone(), cx)); + bottom_panel.update(cx, |panel, cx| { + panel.set_run_editor(Some(editor.clone()), cx); + }); let status_bar = cx.new(|_cx| StatusBarView::new()); let search_everywhere = cx.new(|cx| SearchEverywhereModal::new(cx)); let quick_open = cx.new(|cx| QuickOpenModal::new(cx)); let go_to_line = cx.new(|cx| GoToLineModal::new(cx)); let command_palette = cx.new(|cx| CommandPaletteModal::new(cx)); let settings_dialog = cx.new(|cx| SettingsDialog::new(cx)); + settings_dialog.update(cx, |dialog, cx| { + dialog.set_workspace_root(root.clone(), cx); + }); let project_dialog = cx.new(|cx| ProjectDialog::new(cx)); let branch_manager_root = root.clone(); let branch_manager = cx.new(|cx| BranchManagerView::new(branch_manager_root, cx)); @@ -694,6 +701,12 @@ impl WorkbenchView { SettingsEvent::Changed => { cx.notify(); } + SettingsEvent::RunConfigurationChanged => { + let _ = this.bottom_panel.update(cx, |panel, cx| { + panel.reload_run_project(cx); + }); + cx.notify(); + } }, ); @@ -1010,7 +1023,9 @@ impl WorkbenchView { /// 打开底部 Run 页并运行选中配置:对齐 Tauri `openRunDecisionPane` + /// `runConfiguration`(页签可见 + 受管进程,不进交互终端)。 fn open_run_pane(&mut self, cx: &mut Context) { + let editor = self.active_editor(); let _ = self.bottom_panel.update(cx, |bp, cx| { + bp.set_run_editor(editor, cx); bp.set_tab(BottomTab::Run, cx); bp.run_selected_config(cx); }); @@ -1023,6 +1038,9 @@ impl WorkbenchView { } self.workspace_root = path.clone(); self.show_welcome = false; + let _ = self + .settings_dialog + .update(cx, |dialog, cx| dialog.set_workspace_root(path.clone(), cx)); // 同步工具栏当前项目行(仿分支同步写法)。 let _ = self.toolbar.update(cx, |tb, cx| { tb.set_workspace_root(path.clone(), cx); @@ -1040,6 +1058,7 @@ impl WorkbenchView { let _ = self.bottom_panel.update(cx, |bp, cx| { bp.set_working_dir(path.clone(), cx); }); + self.sync_run_editor(cx); // 右侧通知中心投递项目打开事件(对齐 Tauri 系统事件通知)。 let opened = crate::i18n::menu_text(cx, "notifications.projectOpened").to_string(); let _ = self.notifications.update(cx, |n, cx| { @@ -1114,15 +1133,11 @@ impl WorkbenchView { .and_then(|id| self.pane_editors.get(&id).cloned()) } - /// 新文件路由编辑器(`route_target()` 落 map,未命中回退 active; - /// 锁定窗格自动跳过由模型保证)。 - fn routed_editor(&self) -> Option> { - if let Some(id) = self.pane_tree.route_target() { - if let Some(ed) = self.pane_editors.get(&id).cloned() { - return Some(ed); - } - } - self.active_editor() + fn sync_run_editor(&mut self, cx: &mut Context) { + let editor = self.active_editor(); + let _ = self + .bottom_panel + .update(cx, |panel, cx| panel.set_run_editor(editor, cx)); } /// 编辑器文档变化:选择语言服务器、按需启动会话并同步全文。 @@ -1365,6 +1380,16 @@ impl WorkbenchView { .update(cx, |panel, cx| panel.set_diagnostics(entries, cx)); } + /// 新文件路由编辑器(`route_target()` 落 map,未命中回退 active; + /// 锁定窗格自动跳过由模型保证)。 + fn routed_editor(&self) -> Option> { + if let Some(id) = self.pane_tree.route_target() { + if let Some(ed) = self.pane_editors.get(&id).cloned() { + return Some(ed); + } + } + self.active_editor() + } /// 活动编辑器上执行动作(无活动窗格时忽略)。 fn with_active_editor( @@ -1443,6 +1468,7 @@ impl WorkbenchView { fn close_pane(&mut self, pane_id: PaneId, cx: &mut Context) { if self.pane_tree.close_leaf(pane_id) { self.pane_editors.remove(&pane_id); + self.sync_run_editor(cx); cx.notify(); } } @@ -1975,6 +2001,7 @@ impl WorkbenchView { if let Some(pid) = target_pane { let _ = this.update(cx, |this, cx| { this.pane_tree.set_active(pid); + this.sync_run_editor(cx); cx.notify(); }); } @@ -2370,13 +2397,17 @@ impl WorkbenchView { } } - /// 渲染单个窗格叶:对应 editor;整个叶包左键设 active;空标签叶居中 - /// 显示关闭窗格按钮(根叶关闭返回 `false` 时忽略)。 + /// 渲染单个窗格叶:对应 editor;整个叶包左键设 active。空拆分窗格保留 + /// 编辑器空态,并在右上角提供关闭操作;根窗格不可关闭,不显示该操作。 fn render_pane_leaf(&self, id: PaneId, cx: &mut Context) -> AnyElement { let Some(editor) = self.pane_editors.get(&id).cloned() else { return div().size_full().into_any_element(); }; let is_empty = editor.read(cx).tabs.is_empty(); + let can_close = !matches!( + self.pane_tree.root(), + Some(PaneNode::Leaf(root_id)) if *root_id == id + ); let base = v_flex() .size_full() .min_w_0() @@ -2386,25 +2417,28 @@ impl WorkbenchView { MouseButton::Left, cx.listener(move |this, _event: &MouseDownEvent, _window, cx| { this.pane_tree.set_active(id); + this.sync_run_editor(cx); cx.notify(); }), ); - if is_empty { - base.items_center() - .justify_center() - .child( + let pane = div().relative().size_full().child(editor); + let pane = if is_empty && can_close { + pane.child( + div().absolute().top_1().right_1().child( Button::new(format!("close-pane-{id}")) - .small() + .xsmall() .ghost() - .label(crate::i18n::menu_text(cx, "ui.close").to_string()) + .icon(IconName::Close) + .tooltip(crate::i18n::menu_text(cx, "ui.close")) .on_click(cx.listener(move |this, _event, _window, cx| { this.close_pane(id, cx); })), - ) - .into_any_element() + ), + ) } else { - base.child(editor).into_any_element() - } + pane + }; + base.child(pane).into_any_element() } fn render_sidebar_splitter(&self, cx: &mut Context) -> impl IntoElement { From c9f766e03f0861c5c2f7261705882d8cd7918aaa Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 17:20:47 +0800 Subject: [PATCH 56/59] =?UTF-8?q?feat(linux):=20=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=E6=B4=BE=E7=94=9F=E7=9A=84=E5=85=A8=E5=B1=80?= =?UTF-8?q?=E5=BF=AB=E6=8D=B7=E9=94=AE=E4=B8=8E=E5=8F=8C=E5=87=BB=20Shift?= =?UTF-8?q?=20=E6=90=9C=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Linux GPUI 工作台此前只有少量硬编码快捷键,且与菜单展示的快捷键不一致: 菜单写着 Ctrl+F / Ctrl+Shift+E / Ctrl+Shift+P 等,按下却无响应,原因是 `toolbar.rs` 的菜单表与 `view.rs` 的 `on_key_down` 各写了一份定义。 本次把「菜单表」确立为快捷键唯一真源: - 新增 `linux/src/keybindings.rs` - `KeyStrokeId`:归一化修饰键与主键名(`Ctrl+Shift+P` / `Alt+Up` / `F11`)。 - `KeymapIndex`:启动时从菜单表构建「按键 → 动作 id」索引,并保留无法 解析的写法供自检。 - `DoubleShiftRecognizer`:移植 Tauri `DoubleShiftGestureRecognizer`, 仅统计独立 Shift 的两次抬手,350ms 时间窗内触发;期间出现其它按键或 Ctrl/Alt/Super 会取消手势。 - `toolbar.rs` 新增 `menu_shortcut_entries()`:展平菜单快捷键表,跳过 `disabled` 条目并按菜单顺序去重,避免禁用项(如 `go.references` 的 Ctrl+B)抢走可用项按键。 - `view.rs` 按键监听改为查表分发:命中后编辑器动作经 `editor_direct` 直达(需要 `&mut Window`),其余统一交给既有 `handle_action`,与菜单项、 命令面板共用同一套动作分支;弹窗打开时不抢键,保证输入框正常打字。 - 新增 `.on_modifiers_changed` 监听:双击 Shift 打开全局搜索 (`view.global_search`),打开任意弹窗时重置手势状态,避免跨上下文拼接。 - 启动时自检快捷键表,出现不可解析写法或空表时输出到日志。 生效范围(42 条,全部与菜单展示一致):文件(新建/打开/保存/关闭/重开)、 编辑(撤销/重做/剪贴板/注释/复制行/删除行/上下移动行/查找/替换)、 视图(命令面板/活动栏/侧边栏/底部面板/全局搜索/诊断/资源管理器/Git/ 自动换行/缩放)、跳转(快速打开/跳转行/上下标签)、窗口(最小化/最大化/ 全屏)。双击 Shift 触发搜索与 Tauri/IntelliJ 行为对齐。 验证:`cargo check` 无警告;`cargo test --lib` 33 通过(含 10 条快捷键 与双击 Shift 单测,覆盖解析、冲突、禁用项过滤与时间窗边界)。 --- linux/src/keybindings.rs | 406 +++++++++++++++++++++++++++++++++ linux/src/lib.rs | 1 + linux/src/main.rs | 1 + linux/src/workbench/toolbar.rs | 26 +++ linux/src/workbench/view.rs | 191 +++++++++++----- 5 files changed, 571 insertions(+), 54 deletions(-) create mode 100644 linux/src/keybindings.rs 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 index 559e4cf94..04970fa31 100644 --- a/linux/src/lib.rs +++ b/linux/src/lib.rs @@ -1,5 +1,6 @@ pub mod core; pub mod i18n; +pub mod keybindings; pub mod lsp; pub mod settings; pub mod theme; diff --git a/linux/src/main.rs b/linux/src/main.rs index d89dd7899..ea953fb16 100644 --- a/linux/src/main.rs +++ b/linux/src/main.rs @@ -2,6 +2,7 @@ mod core; mod i18n; +mod keybindings; mod lsp; mod settings; mod theme; diff --git a/linux/src/workbench/toolbar.rs b/linux/src/workbench/toolbar.rs index 5a8e0d756..0336cdd28 100644 --- a/linux/src/workbench/toolbar.rs +++ b/linux/src/workbench/toolbar.rs @@ -373,6 +373,32 @@ const APP_MENUS: &[AppMenu] = &[ }, ]; +/// 菜单表里所有非空快捷键的 `(展示文本, 动作 id)` 展平结果,按菜单顺序稳定。 +/// +/// 这是快捷键的唯一真源:`keybindings` 模块据此生成按键索引, +/// 因此「菜单上写着什么快捷键」与「按什么键会触发」不会出现两份定义。 +/// +/// 只收录可用条目(`disabled` 的菜单项后端能力缺失,绑定后按键按下会静默 +/// 无响应,比未绑定更难排查)。同一按键被多个可用条目占用时,取菜单中 +/// 先出现的那个,保证结果与菜单自上而下的阅读顺序一致。 +pub fn menu_shortcut_entries() -> Vec<(&'static str, &'static str)> { + let mut out: Vec<(&'static str, &'static str)> = Vec::new(); + for menu in APP_MENUS { + for group in menu.groups { + for item in *group { + if item.shortcut.is_empty() || item.disabled { + continue; + } + if out.iter().any(|(shortcut, _)| *shortcut == item.shortcut) { + continue; + } + out.push((item.shortcut, item.action)); + } + } + } + out +} + /// 构造一个发出 `MenuAction(id)` 的菜单项。 /// /// 用 `PopupMenuItem::element` 自定义行:左侧标签占满(`text_xs`),右侧快捷键 diff --git a/linux/src/workbench/view.rs b/linux/src/workbench/view.rs index bcbf39db8..5463a5fbb 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -10,8 +10,9 @@ use gpui_kit::component::{h_flex, v_flex, Sizable as _}; use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::{ div, px, AnyElement, App, AppContext as _, Context, Entity, FocusHandle, - InteractiveElement as _, IntoElement, KeyDownEvent, MouseButton, MouseDownEvent, - MouseMoveEvent, MouseUpEvent, ParentElement as _, Render, Styled as _, Subscription, Window, + InteractiveElement as _, IntoElement, KeyDownEvent, ModifiersChangedEvent, MouseButton, + MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Render, Styled as _, + Subscription, Window, }; use std::collections::{HashMap, HashSet}; @@ -153,6 +154,13 @@ pub struct WorkbenchView { /// 欢迎页 pub welcome_screen: Entity, + /// 快捷键索引(菜单快捷键表 → 动作 id),启动时构建一次。 + keymap: crate::keybindings::KeymapIndex, + /// 双击 Shift 手势识别器(全局搜索)。 + double_shift: crate::keybindings::DoubleShiftRecognizer, + /// 双击 Shift 计时用的单调起点(进程内首次按键时惰性记录)。 + double_shift_clock_origin: Option, + focus_handle: FocusHandle, client: CoreClient, _subscriptions: Vec, @@ -863,6 +871,9 @@ impl WorkbenchView { project_dialog, branch_manager, welcome_screen, + keymap: crate::keybindings::KeymapIndex::build(), + double_shift: crate::keybindings::DoubleShiftRecognizer::new(), + double_shift_clock_origin: None, focus_handle, client: CoreClient::new(), _subscriptions: vec![ @@ -895,6 +906,18 @@ impl WorkbenchView { !crate::theme::ThemePalette::is_light(&settings::resolved_theme_id(s, false)) }; view.apply_resolved_theme(is_dark, cx); + // 快捷键表自检:菜单写了但无法解析的快捷键会让「菜单显示了键、按下却无效」 + // 变得很难排查,启动时直接报到日志(正常情况下为空)。若整张表为空, + // 说明菜单表与解析器已经脱钩,同样算错。 + if view.keymap.is_empty() || !view.keymap.invalid_shortcuts().is_empty() { + let bad = view.keymap.invalid_shortcuts().join(", "); + let msg = format!( + "[Keymap] {} shortcut(s) parsed, unparseable: {}", + view.keymap.len(), + bad + ); + view.append_log(&msg, cx); + } // 启动目录同样记入最近项目(对齐 Tauri 打开即记录)。 settings::record_recent_project(cx, &view.workspace_root.clone()); // 同步工具栏当前项目行(仿分支同步写法;构造时已传入,此处再确认一次)。 @@ -1077,6 +1100,9 @@ impl WorkbenchView { pub fn open_search_everywhere(&mut self, cx: &mut Context) { this_sync_files(self, cx); self.show_search_everywhere = true; + // 弹窗打开后 Shift 用于输入,手势状态必须清空, + // 避免关闭弹窗后把之前的一次 Shift 与下一次拼接成双击。 + self.double_shift.reset(); let _ = self.search_everywhere.update(cx, |search, cx| { search.reset(cx); }); @@ -1087,6 +1113,7 @@ impl WorkbenchView { pub fn open_quick_open(&mut self, cx: &mut Context) { this_sync_files(self, cx); self.show_quick_open = true; + self.double_shift.reset(); let _ = self.quick_open.update(cx, |qo, cx| { qo.reset(cx); }); @@ -1096,6 +1123,7 @@ impl WorkbenchView { /// 打开跳转到行弹窗 pub fn open_go_to_line(&mut self, cx: &mut Context) { self.show_go_to_line = true; + self.double_shift.reset(); let _ = self.go_to_line.update(cx, |modal, cx| { modal.reset(cx); }); @@ -1105,6 +1133,7 @@ impl WorkbenchView { /// 打开命令面板 pub fn open_command_palette(&mut self, cx: &mut Context) { self.show_command_palette = true; + self.double_shift.reset(); let _ = self.command_palette.update(cx, |cp, cx| { cp.reset(cx); }); @@ -1133,6 +1162,28 @@ impl WorkbenchView { .and_then(|id| self.pane_editors.get(&id).cloned()) } + /// 是否有模态浮层可见。可见时按键监听不抢快捷键,让弹窗输入框正常接收字符。 + fn modal_visible(&self) -> bool { + self.show_search_everywhere + || self.show_quick_open + || self.show_go_to_line + || self.show_command_palette + || self.show_settings_dialog + || self.show_project_dialog + || self.show_branch_manager + } + + /// 双击 Shift 用的单调秒级时钟。 + /// + /// 识别器只关心两次抬手的时间差,因此以「首次调用时刻」为零点, + /// 避免依赖墙钟(系统时间跳变会让手势失效)。 + fn double_shift_clock(&mut self) -> f64 { + let origin = *self + .double_shift_clock_origin + .get_or_insert_with(std::time::Instant::now); + origin.elapsed().as_secs_f64() + } + fn sync_run_editor(&mut self, cx: &mut Context) { let editor = self.active_editor(); let _ = self @@ -2108,7 +2159,6 @@ impl Render for WorkbenchView { .bg(ThemeColors::background()) .on_key_down(cx.listener(|this, event: &KeyDownEvent, window, cx| { let key = event.keystroke.key.as_str(); - let modifiers = event.keystroke.modifiers; // 浮层优先:Esc 关闭最上层模态。 if key == "escape" { @@ -2129,60 +2179,93 @@ impl Render for WorkbenchView { return; } - if modifiers.control || modifiers.platform { - if modifiers.shift && key == "p" { - this.open_command_palette(cx); - return; - } - if modifiers.shift && key == "f" { - this.open_search_everywhere(cx); - return; - } - match key { - "p" => this.open_quick_open(cx), - "," => this.open_settings(cx), - "b" => this.toggle_sidebar(cx), - "`" => { - let _ = this.bottom_panel.update(cx, |bp, cx| { - bp.toggle_collapsed(cx); - }); - } - "j" => { - let _ = this.bottom_panel.update(cx, |bp, cx| { - bp.set_tab(BottomTab::Terminal, cx); - }); - } - // 行操作快捷键与菜单展示一致(上游编辑器不原生支持)。 - // 按键监听自带 `window`,直接下发,避免经句柄二次 - // `update` 的重入借用失败。 - "d" => this.editor_direct(window, cx, |ed, window, cx| { - ed.duplicate_line(window, cx); - }), - "/" => this.editor_direct(window, cx, |ed, window, cx| { - ed.toggle_comment(window, cx); - }), - _ => { - if modifiers.shift && (key == "k" || key == "K") { - this.editor_direct(window, cx, |ed, window, cx| { - ed.delete_line(window, cx); - }); - } - } - } - // Alt+Up/Down 移动行(`modifiers.alt`)。 - if modifiers.alt && (key == "up" || key == "down") { - if key == "up" { - this.editor_direct(window, cx, |ed, window, cx| { - ed.move_line_up(window, cx); - }); - } else { - this.editor_direct(window, cx, |ed, window, cx| { - ed.move_line_down(window, cx); - }); - } + // 弹窗打开时输入框要能正常打字:除 Esc 外不抢占快捷键, + // 与 Tauri 模态捕获按键的语义一致。 + if this.modal_visible() { + return; + } + + // 其它按键按下:本次 Shift 按压不再算「独立」,双击手势取消。 + // 注意 Linux 后端不为修饰键本身发 `KeyDownEvent`(X11 侧 + // `keysym.is_modifier_key()` 直接返回),因此这里只会收到 + // 真正的普通按键,Shift 的按下/抬起完全由 `ModifiersChanged` 驱动。 + this.double_shift.handle_key_down(); + + // 快捷键统一走菜单派生的按键表;命中后交给 `handle_action`, + // 与菜单项、命令面板共用同一套动作分支。 + let stroke = crate::keybindings::KeyStrokeId::from_event( + key, + &event.keystroke.modifiers, + ); + let Some(action_id) = this.keymap.lookup(&stroke) else { + return; + }; + // 编辑器动作需要 `&mut Window`,经 `editor_direct` 直达; + // 其余动作不重入窗口,统一由 `handle_action` 处理。 + match action_id { + "edit.duplicate_line" => this.editor_direct(window, cx, |ed, window, cx| { + ed.duplicate_line(window, cx); + }), + "edit.delete_line" => this.editor_direct(window, cx, |ed, window, cx| { + ed.delete_line(window, cx); + }), + "edit.move_up" => this.editor_direct(window, cx, |ed, window, cx| { + ed.move_line_up(window, cx); + }), + "edit.move_down" => this.editor_direct(window, cx, |ed, window, cx| { + ed.move_line_down(window, cx); + }), + "edit.toggle_comment" => this.editor_direct(window, cx, |ed, window, cx| { + ed.toggle_comment(window, cx); + }), + "edit.undo" => this.editor_direct(window, cx, |ed, window, cx| { + ed.undo(window, cx); + }), + "edit.redo" => this.editor_direct(window, cx, |ed, window, cx| { + ed.redo(window, cx); + }), + "edit.cut" => this.editor_direct(window, cx, |ed, window, cx| { + ed.cut(window, cx); + }), + "edit.paste" => this.editor_direct(window, cx, |ed, window, cx| { + ed.paste(window, cx); + }), + "edit.select_all" => this.editor_direct(window, cx, |ed, window, cx| { + ed.select_all(window, cx); + }), + "edit.copy" => this.with_active_editor(cx, |ed, cx| { + ed.copy(cx); + }), + "view.toggle_wrap" => this.editor_direct(window, cx, |ed, window, cx| { + ed.toggle_wrap(window, cx); + }), + "view.toggle_line_numbers" => { + this.editor_direct(window, cx, |ed, window, cx| { + ed.toggle_line_numbers(window, cx); + }) } + "view.toggle_whitespace" => this.editor_direct(window, cx, |ed, window, cx| { + ed.toggle_whitespace(window, cx); + }), + other => this.handle_action(other, cx), } })) + // 双击 Shift:GPUI 的修饰键变化事件包含按下/抬起两次, + // 由识别器按时间窗判定,命中后打开全局搜索。 + .on_modifiers_changed( + cx.listener(|this, event: &ModifiersChangedEvent, _window, cx| { + let timestamp = this.double_shift_clock(); + let modifiers = event.modifiers; + let has_other = modifiers.control || modifiers.alt || modifiers.platform; + if this.double_shift.handle_modifiers_changed( + modifiers.shift, + has_other, + timestamp, + ) { + this.handle_action(crate::keybindings::DOUBLE_SHIFT_ACTION, cx); + } + }), + ) .on_mouse_move(cx.listener(|this, event: &MouseMoveEvent, _window, cx| { if this.is_resizing_sidebar { let current_x = f32::from(event.position.x); From 72e2ef4d023c23bf62bc484d5d95138b23390098 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 17:21:12 +0800 Subject: [PATCH 57/59] =?UTF-8?q?feat(linux):=20=E6=8E=A5=E5=85=A5=20JDT?= =?UTF-8?q?=20LS=20=E7=9A=84=20Java=20=E9=A1=B9=E7=9B=AE=E5=87=86=E5=A4=87?= =?UTF-8?q?=E4=B8=8E=20Run=20=E7=9B=B4=E5=90=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Linux 第二阶段补齐 Java Main / Spring Boot 的 Core 直启链路,与 macOS/Windows 走同一条 Java 项目准备边界,不再依赖 Maven goal 启动。 - `linux/src/lsp.rs` - 解析 JDT LS 直启资源:优先 `LITHE_JDTLS_ROOT` / `LITHE_JDK_ROOT`, 默认工作区 `.artifacts/jdtls-linux` 与 `.artifacts/jdk-linux`; equinox launcher / debug plugin 按版本排序,资源目录存在但不完整时 返回可操作错误,外部 `jdtls` wrapper 仅作兼容回退。 - 提供 workspace fingerprint、`javaLaunch` 请求形状与入口 / classpath / build 结果的解析(不扫源码、不在 UI 拼 classpath)。 - 会话生命周期:Core `java` provider、`ServiceReady`、 `projectPreparation.blocksRun` 快照、过期 session/sequence 拒绝。 - `linux/src/workbench/bottom_panel.rs` - Maven-backed `java.main` 与带 Java 入口的 Spring Boot 先等待 session ready,再按 `javaEntrypoints` → `vscode.java.buildWorkspace` → `vscode.java.resolveClasspath` 顺序准备,把 JDT 返回的 `{mainClass, projectName, classPaths, modulePaths}` 作为 `javaLaunch` 交给 `runConfig.createLaunchPlan`。 - 构建失败保留 Core build diagnostics 并阻止主进程,可通过再次 Run 重试; 无 JDT 元数据时 framework goal 与独立 Java `javac -> java` 仍走既有计划。 - `view.rs` 持有唯一 `java` session,处理 workspace 失效与诊断聚合。 - 更新 Agent 笔记,记录第二阶段的 Linux 边界与验证方式。 验证:`cargo check` 无警告;`cargo test --lib` 40 通过(覆盖 JDT 资源排序、 载荷形状、入口/classpath/build 解析、provider 门禁、过期 session/sequence 与进程组停止);测试不依赖本机 Maven/JDK/网络。 --- ...-java-project-build-and-launch-boundary.md | 32 +- linux/src/lsp.rs | 1077 ++++++++++++++++- linux/src/workbench/bottom_panel.rs | 493 +++++++- linux/src/workbench/view.rs | 448 ++++++- 4 files changed, 1992 insertions(+), 58 deletions(-) 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 4339455f0..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,12 +36,23 @@ classpath/module-path;随后 Run 模块只启动一次项目 JDK。Maven 仍 4. Rust Core 接收结构化 `javaLaunch`,生成 `project-jdk` 直启计划; 5. macOS/Windows 宿主按各自路径分隔符拼接参数并启动一个 JVM。 -Linux 第一阶段沿用同一条配置边界:`bottom_panel.rs` 只发 Core 命令和展示 -状态,`workbench/run/config.rs` 持久化 Core 生成结果、解析工具链与启动计划, -`workbench/run/process.rs` 独占 session、子进程组和输出事件。Linux 不读取旧 -`name/type/mainClass` 文档作为主路径,不再用源码正则补入口,也不在计划失败 -后伪造 `mvn compile`。Maven 项目里的 `java.main` 仍因缺少 JDT 返回的 -`javaLaunch` 明确失败,直到 Linux 接入同一 Java 项目准备能力。 +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、测试与显式 @@ -214,8 +225,10 @@ Run 控件,也不在 `java-run-launch` 或平台 adapter 再做一次 `blocksR Run/Debug 共用的 Store 测试覆盖同一次启动继续、取消、记忆选择和索引恢复。 - Linux:`cargo test --manifest-path linux/Cargo.toml --lib --bins` 覆盖 Core process/Spring Boot 计划、`javac -> java`、POSIX classpath、工具链映射、 - pre-launch 失败门禁、过期 sequence 与完整进程组停止;测试不依赖本机 - Maven/JDK/网络。 + 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`。 @@ -227,6 +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/workbench/bottom_panel.rs`、`linux/src/workbench/run/` +- 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/linux/src/lsp.rs b/linux/src/lsp.rs index c9ebc6f4d..14f6b5cdb 100644 --- a/linux/src/lsp.rs +++ b/linux/src/lsp.rs @@ -5,7 +5,10 @@ //! 平台侧的语言/可执行文件探测、路径与 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; @@ -25,6 +28,86 @@ pub struct LanguageProvider { 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", @@ -66,7 +149,7 @@ const GOPLS: LanguageProvider = LanguageProvider { }; const JDTLS: LanguageProvider = LanguageProvider { - id: "jdtls", + id: "java", executable: "jdtls", arguments: &[], language_id: "java", @@ -131,7 +214,757 @@ pub fn find_in_path(name: &str) -> Option { 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() { @@ -208,16 +1041,11 @@ pub fn start_payload( root: &str, cache_directory: &str, ) -> Value { - let mut arguments: Vec = provider + let arguments: Vec = provider .arguments .iter() .map(|value| value.to_string()) .collect(); - if provider.id == "jdtls" { - // jdtls 要求显式 data 目录;configuration 目录由发行包脚本补齐。 - arguments.push("-data".to_string()); - arguments.push(cache_directory.to_string()); - } json!({ "providerId": provider.id, "executablePath": executable, @@ -252,6 +1080,7 @@ pub fn poll_payload(session_id: &str) -> Value { } /// 语言服务器退出后 Core 只允许在收到终态事件后销毁会话。 +#[allow(dead_code)] pub fn session_finished(value: &Value) -> bool { value .get("events") @@ -266,6 +1095,24 @@ pub fn session_finished(value: &Value) -> bool { .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(); @@ -321,6 +1168,19 @@ pub fn diagnostics_from_poll(value: &Value, root: &str) -> HashMap 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(), @@ -355,6 +1215,10 @@ mod tests { 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); } @@ -388,4 +1252,201 @@ mod tests { 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/workbench/bottom_panel.rs b/linux/src/workbench/bottom_panel.rs index be61351f9..f8f015e78 100644 --- a/linux/src/workbench/bottom_panel.rs +++ b/linux/src/workbench/bottom_panel.rs @@ -1,4 +1,5 @@ use std::sync::mpsc; +use std::time::{Duration, Instant}; use gpui_kit::assets::IconName; use gpui_kit::component::button::{Button, ButtonVariants as _}; @@ -9,10 +10,11 @@ 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 _, Window, + Styled as _, WeakEntity, Window, }; use crate::core::CoreClient; +use crate::lsp; use crate::theme::ThemeColors; use crate::workbench::editor::EditorView; use crate::workbench::run::{ @@ -22,6 +24,7 @@ use crate::workbench::run::{ ProcessEvent, ProcessManager, RunConfigItem, }; use crate::workbench::terminal::TerminalView; +use crate::workbench::view::WorkbenchView; /// 运行历史上限。 const MAX_RUN_HISTORY: usize = 50; @@ -142,6 +145,8 @@ pub struct BottomPanelView { 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` 结果。 @@ -162,6 +167,269 @@ pub struct BottomPanelView { 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)); @@ -192,6 +460,7 @@ impl BottomPanelView { maven_output: Vec::new(), maven_running: false, client: CoreClient::new(), + workbench: None, processes: ProcessManager::new(), maven_seq: 0, last_maven_goal: None, @@ -359,6 +628,12 @@ impl BottomPanelView { 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; @@ -378,6 +653,7 @@ impl BottomPanelView { 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::( @@ -423,15 +699,44 @@ impl BottomPanelView { }; 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", - serde_json::json!({ - "root": root, - "paths": paths, - "modulePaths": [] - }), + generate_payload, ) .await; let generated = match generated { @@ -615,14 +920,26 @@ impl BottomPanelView { 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 { @@ -661,12 +978,70 @@ impl BottomPanelView { 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(), - None, + java_launch.as_ref().map(|(value, _)| value), ); let plan = client .execute::( @@ -1078,6 +1453,23 @@ impl BottomPanelView { } } + /// 判断配置是否必须由 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) @@ -1818,3 +2210,90 @@ fn git_log_failed_text(cx: &gpui_kit::App) -> &'static str { "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/view.rs b/linux/src/workbench/view.rs index 5463a5fbb..4b5fa14e3 100644 --- a/linux/src/workbench/view.rs +++ b/linux/src/workbench/view.rs @@ -47,6 +47,8 @@ use crate::workbench::tab_menu::{SplitCallback, ToggleLockCallback}; use crate::workbench::toolbar::{ToolbarEvent, ToolbarView}; use crate::workbench::welcome_screen::{WelcomeEvent, WelcomeScreenView}; +const MAX_LSP_OPERATION_RESULTS: usize = 256; + /// 右侧工具窗口当前视图,对齐 Tauri `activeRightSidebarView` ///(`notifications` / `maven` / 扩展)。`None` 即隐藏,不持久化,重启丢失。 #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -127,6 +129,16 @@ pub struct WorkbenchView { lsp_pending_documents: HashMap>, /// LSP:工作区相对路径 → 该文件最新诊断,供底部面板聚合展示。 lsp_diagnostics: HashMap>, + /// 工作区代数;切换项目后旧 LSP 结果不能写回新工作区。 + lsp_workspace_generation: u64, + /// Java session 的展示状态;Run 只通过 Workbench 读取它。 + java_lsp_state: String, + /// Java session 最近一次启动或轮询错误。 + java_lsp_error: Option, + /// Core 轮询返回的 Java 项目准备快照。 + java_lsp_project_preparation: Option, + /// 已完成但尚未由 Run 取走的 Core LSP 请求结果。 + lsp_operation_results: HashMap, /// 侧边栏面板 (Files / Git / Search) pub sidebar: Entity, /// 多窗格布局模型(`Split{Horizontal}` 左右并排,`{Vertical}` 上下堆叠) @@ -189,8 +201,10 @@ impl WorkbenchView { pane_editors.insert(initial_pane, editor.clone()); Self::wire_pane_editor(&pane_tree, cx.entity(), initial_pane, &editor, cx); let bottom_panel = cx.new(|cx| BottomPanelView::new(root.clone(), cx)); + let workbench_entity = cx.entity(); bottom_panel.update(cx, |panel, cx| { panel.set_run_editor(Some(editor.clone()), cx); + panel.set_workbench(workbench_entity.clone(), cx); }); let status_bar = cx.new(|_cx| StatusBarView::new()); let search_everywhere = cx.new(|cx| SearchEverywhereModal::new(cx)); @@ -852,6 +866,11 @@ impl WorkbenchView { lsp_document_versions: HashMap::new(), lsp_pending_documents: HashMap::new(), lsp_diagnostics: HashMap::new(), + lsp_workspace_generation: 0, + java_lsp_state: "idle".to_string(), + java_lsp_error: None, + java_lsp_project_preparation: None, + lsp_operation_results: HashMap::new(), toolbar, activity_rail, plugin_rail, @@ -1055,10 +1074,46 @@ impl WorkbenchView { cx.notify(); } + fn lsp_invalidate_workspace(&mut self, cx: &mut Context) { + self.lsp_workspace_generation = self.lsp_workspace_generation.wrapping_add(1); + let sessions = self.lsp_sessions.drain().collect::>(); + self.lsp_starting.clear(); + self.lsp_polling.clear(); + self.lsp_document_versions.clear(); + self.lsp_pending_documents.clear(); + self.lsp_diagnostics.clear(); + self.lsp_operation_results.clear(); + self.java_lsp_state = "idle".to_string(); + self.java_lsp_error = None; + self.java_lsp_project_preparation = None; + self.sync_diagnostics_panel(cx); + for (_provider_id, session) in sessions { + let client = self.client.clone(); + cx.spawn(async move |_this, cx| { + let _ = client + .execute::<_, serde_json::Value>( + &cx, + "lsp.stopServer", + serde_json::json!({ "sessionId": session }), + ) + .await; + let _ = client + .execute::<_, serde_json::Value>( + &cx, + "lsp.destroyServer", + serde_json::json!({ "sessionId": session }), + ) + .await; + }) + .detach(); + } + } + fn open_project_path(&mut self, path: String, cx: &mut Context) { if self.show_project_dialog { self.show_project_dialog = false; } + self.lsp_invalidate_workspace(cx); self.workspace_root = path.clone(); self.show_welcome = false; let _ = self @@ -1191,25 +1246,95 @@ impl WorkbenchView { .update(cx, |panel, cx| panel.set_run_editor(editor, cx)); } + /// 返回 Run 所见的 Java session 状态。 + pub fn java_lsp_status(&self) -> lsp::JavaLspStatus { + lsp::JavaLspStatus { + session_id: self.lsp_sessions.get(lsp::JAVA_PROVIDER_ID).cloned(), + state: self.java_lsp_state.clone(), + error: self.java_lsp_error.clone(), + generation: self.lsp_workspace_generation, + project_preparation: self.java_lsp_project_preparation.clone(), + } + } + + /// 确保当前工作区拥有唯一的 Java Core session。 + pub fn ensure_java_lsp(&mut self, cx: &mut Context) { + if self.lsp_sessions.contains_key(lsp::JAVA_PROVIDER_ID) + || self.lsp_starting.contains(lsp::JAVA_PROVIDER_ID) + { + return; + } + let root = self.workspace_root.clone(); + let launch = match lsp::resolve_java_lsp_launch(&root) { + Ok(launch) => launch, + Err(error) => { + self.lsp_push_error(lsp::JAVA_PROVIDER_ID, &error, cx); + return; + } + }; + self.lsp_start_java_server(launch, root, cx); + } + + /// 取走一个属于指定 session 的请求结果;旧 session 的结果永远不能被 Run 消费。 + pub fn take_lsp_operation_result( + &mut self, + session_id: &str, + operation_id: &str, + ) -> Option> { + let key = format!("{session_id}\u{0}{operation_id}"); + let result = self.lsp_operation_results.remove(&key)?; + if result.session_id != session_id { + return None; + } + Some(match result.error { + Some(error) => Err(error), + None => result + .result + .ok_or_else(|| "Language-server request returned no result.".to_string()), + }) + } + /// 编辑器文档变化:选择语言服务器、按需启动会话并同步全文。 - /// 未知语言或本机无对应可执行文件时静默跳过,不影响编辑。 + /// Java 文档使用 Core `java` provider 和平台解析的 JDT LS 资源。 fn lsp_on_document_changed(&mut self, path: String, text: String, cx: &mut Context) { - let Some((provider, executable)) = lsp::resolve_provider(&path) else { + let Some(provider) = lsp::provider_for_path(&path) else { return; }; let root = self.workspace_root.clone(); let uri = lsp::file_uri(&root, &path); let language_id = lsp::language_id_for_path(&path, provider).to_string(); + if provider.id == lsp::JAVA_PROVIDER_ID { + if let Some(session) = self.lsp_sessions.get(provider.id).cloned() { + self.lsp_sync_document(&session, uri, language_id, text, cx); + } else { + self.queue_pending_document(provider.id, uri, language_id, text); + self.ensure_java_lsp(cx); + } + return; + } + + let Some((_, executable)) = lsp::resolve_provider(&path) else { + return; + }; if let Some(session) = self.lsp_sessions.get(provider.id).cloned() { self.lsp_sync_document(&session, uri, language_id, text, cx); return; } + self.queue_pending_document(provider.id, uri, language_id, text); + self.lsp_start_server(provider, executable, root, cx); + } - // 会话未就绪:先暂存最新全文,会话 ready 后统一 flush。 + fn queue_pending_document( + &mut self, + provider_id: &str, + uri: String, + language_id: String, + text: String, + ) { let pending = self .lsp_pending_documents - .entry(provider.id.to_string()) + .entry(provider_id.to_string()) .or_default(); if let Some(existing) = pending.iter_mut().find(|document| document.uri == uri) { existing.text = text; @@ -1220,10 +1345,9 @@ impl WorkbenchView { text, }); } - self.lsp_start_server(provider, executable, root, cx); } - /// 启动一个语言服务器会话;同一 provider 同时只允许一个启动任务。 + /// 启动一个非 Java 语言服务器会话;同一 provider 同时只允许一个启动任务。 fn lsp_start_server( &mut self, provider: LanguageProvider, @@ -1231,36 +1355,207 @@ impl WorkbenchView { root: String, cx: &mut Context, ) { - if self.lsp_starting.contains(provider.id) || self.lsp_sessions.contains_key(provider.id) { + let provider_id = provider.id.to_string(); + if self.lsp_starting.contains(&provider_id) || self.lsp_sessions.contains_key(&provider_id) + { return; } - self.lsp_starting.insert(provider.id.to_string()); + self.lsp_starting.insert(provider_id.clone()); + let cache_directory = format!("{root}/.lithe/lsp/{provider_id}"); + let payload = lsp::start_payload(provider, &executable, &root, &cache_directory); + self.spawn_lsp_start(provider_id, payload, cx); + } + fn lsp_start_java_server( + &mut self, + launch: lsp::JavaLspLaunch, + root: String, + cx: &mut Context, + ) { + let provider_id = lsp::JAVA_PROVIDER_ID.to_string(); + if self.lsp_starting.contains(&provider_id) || self.lsp_sessions.contains_key(&provider_id) + { + return; + } + self.lsp_starting.insert(provider_id.clone()); + self.java_lsp_state = "starting".to_string(); + self.java_lsp_error = None; + self.java_lsp_project_preparation = None; + self.lsp_diagnostics.remove("__lsp__java"); + self.sync_diagnostics_panel(cx); let client = self.client.clone(); - let cache_directory = format!("{root}/.lithe/lsp/{}", provider.id); - let payload = lsp::start_payload(provider, &executable, &root, &cache_directory); + let generation = self.lsp_workspace_generation; + let cache_directory = format!("{root}/.lithe/lsp/java"); + let fingerprint_request = lsp::workspace_fingerprint_request(&root, &launch.jdtls_version); + let maven_context = lsp::maven_context_for_workspace(&root); cx.spawn(async move |this, cx| { + let fingerprint = match client + .execute::( + &cx, + "java.jdtWorkspaceFingerprint", + fingerprint_request, + ) + .await + { + Ok(value) => value + .get("workspaceFingerprint") + .and_then(serde_json::Value::as_str) + .map(str::to_string), + Err(error) => { + let _ = this.update(cx, |this, cx| { + if this.lsp_workspace_generation == generation { + this.lsp_starting.remove(lsp::JAVA_PROVIDER_ID); + this.lsp_push_error(lsp::JAVA_PROVIDER_ID, &error, cx); + } + }); + return; + } + }; + let Some(fingerprint) = fingerprint else { + let _ = this.update(cx, |this, cx| { + if this.lsp_workspace_generation == generation { + this.lsp_starting.remove(lsp::JAVA_PROVIDER_ID); + this.lsp_push_error( + lsp::JAVA_PROVIDER_ID, + "java.jdtWorkspaceFingerprint returned no workspace fingerprint", + cx, + ); + } + }); + return; + }; + let payload = lsp::java_start_payload( + &launch, + &root, + &cache_directory, + Some(&fingerprint), + maven_context.as_ref(), + ); let result = client .execute::<_, serde_json::Value>(&cx, "lsp.startServer", payload) .await; - let _ = this.update(cx, |this, cx| { - this.lsp_starting.remove(provider.id); - match result { - Ok(value) => match lsp::parse_session_id(&value) { - Some(session) => { - this.lsp_sessions - .insert(provider.id.to_string(), session.clone()); - this.lsp_start_poll(provider.id, session.clone(), cx); - this.lsp_flush_pending(provider.id, &session, cx); - } - None => { - this.lsp_push_error(provider.id, "lsp.startServer 未返回 sessionId", cx) - } - }, - Err(error) => this.lsp_push_error(provider.id, &error, cx), + let stale_session = result.as_ref().ok().and_then(lsp::parse_session_id); + let accepted = this + .update(cx, |this, cx| { + if this.lsp_workspace_generation != generation { + return false; + } + this.lsp_starting.remove(lsp::JAVA_PROVIDER_ID); + match result { + Ok(value) => match lsp::parse_session_id(&value) { + Some(session) => { + this.lsp_sessions + .insert(lsp::JAVA_PROVIDER_ID.to_string(), session.clone()); + this.java_lsp_state = value + .get("state") + .and_then(serde_json::Value::as_str) + .unwrap_or("initializing") + .to_string(); + this.java_lsp_error = None; + this.lsp_start_poll( + lsp::JAVA_PROVIDER_ID.to_string(), + session.clone(), + generation, + cx, + ); + this.lsp_flush_pending(lsp::JAVA_PROVIDER_ID, &session, cx); + } + None => this.lsp_push_error( + lsp::JAVA_PROVIDER_ID, + "lsp.startServer 未返回 sessionId", + cx, + ), + }, + Err(error) => this.lsp_push_error(lsp::JAVA_PROVIDER_ID, &error, cx), + } + cx.notify(); + true + }) + .unwrap_or(false); + if !accepted { + if let Some(session) = stale_session { + let _ = client + .execute::<_, serde_json::Value>( + &cx, + "lsp.stopServer", + serde_json::json!({ "sessionId": session }), + ) + .await; + let _ = client + .execute::<_, serde_json::Value>( + &cx, + "lsp.destroyServer", + serde_json::json!({ "sessionId": session }), + ) + .await; } - cx.notify(); - }); + } + }) + .detach(); + } + + fn spawn_lsp_start( + &mut self, + provider_id: String, + payload: serde_json::Value, + cx: &mut Context, + ) { + let client = self.client.clone(); + let generation = self.lsp_workspace_generation; + cx.spawn(async move |this, cx| { + let result = client + .execute::<_, serde_json::Value>(&cx, "lsp.startServer", payload) + .await; + let stale_session = result.as_ref().ok().and_then(lsp::parse_session_id); + let accepted = this + .update(cx, |this, cx| { + if this.lsp_workspace_generation != generation { + return false; + } + this.lsp_starting.remove(&provider_id); + match result { + Ok(value) => match lsp::parse_session_id(&value) { + Some(session) => { + this.lsp_sessions + .insert(provider_id.clone(), session.clone()); + this.lsp_start_poll( + provider_id.clone(), + session.clone(), + generation, + cx, + ); + this.lsp_flush_pending(&provider_id, &session, cx); + } + None => this.lsp_push_error( + &provider_id, + "lsp.startServer 未返回 sessionId", + cx, + ), + }, + Err(error) => this.lsp_push_error(&provider_id, &error, cx), + } + cx.notify(); + true + }) + .unwrap_or(false); + if !accepted { + if let Some(session) = stale_session { + let _ = client + .execute::<_, serde_json::Value>( + &cx, + "lsp.stopServer", + serde_json::json!({ "sessionId": session }), + ) + .await; + let _ = client + .execute::<_, serde_json::Value>( + &cx, + "lsp.destroyServer", + serde_json::json!({ "sessionId": session }), + ) + .await; + } + } }) .detach(); } @@ -1291,7 +1586,9 @@ impl WorkbenchView { cx: &mut Context, ) { let client = self.client.clone(); + let session_id = session.to_string(); let key = format!("{session}\u{0}{uri}"); + let generation = self.lsp_workspace_generation; let payload = lsp::sync_payload(session, &uri, &language_id, &text); cx.spawn(async move |this, cx| { let result = client @@ -1302,11 +1599,18 @@ impl WorkbenchView { .get("documentVersion") .and_then(serde_json::Value::as_i64); let _ = this.update(cx, |this, _cx| { + if this.lsp_workspace_generation != generation + || !this + .lsp_sessions + .values() + .any(|current| current == &session_id) + { + return; + } match version { Some(version) => { this.lsp_document_versions.insert(key, version); } - // 保留会话-文档映射,便于后续清理时识别归属。 None => { this.lsp_document_versions.entry(key).or_insert(0); } @@ -1320,8 +1624,9 @@ impl WorkbenchView { /// 为会话启动唯一的事件轮询循环,直至服务器停止或命令失败。 fn lsp_start_poll( &mut self, - provider_id: &'static str, + provider_id: String, session: String, + generation: u64, cx: &mut Context, ) { if !self.lsp_polling.insert(session.clone()) { @@ -1337,23 +1642,85 @@ impl WorkbenchView { .await; let finished = match result { Ok(value) => { - let diagnostics = lsp::diagnostics_from_poll(&value, &root); - let finished = lsp::session_finished(&value); - let provider = provider_id.to_string(); + let diagnostics = lsp::diagnostics_from_poll_for(&value, &root, &session); + let finished = lsp::session_finished_for(&value, &session); + let results = lsp::operation_results_from_poll(&value, &session); + let java_state = lsp::java_lifecycle_state_for(&value, Some(&session)); + let preparation = value.get("projectPreparation").cloned(); + let provider = provider_id.clone(); let session_key = session.clone(); let _ = this.update(cx, |this, cx| { + if this.lsp_workspace_generation != generation + || this.lsp_sessions.get(&provider).map(String::as_str) + != Some(session_key.as_str()) + { + return; + } if !diagnostics.is_empty() { this.lsp_apply_diagnostics(diagnostics, cx); } + for (operation_id, result) in results { + let key = format!("{session_key}\u{0}{operation_id}"); + if !this.lsp_operation_results.contains_key(&key) + && this.lsp_operation_results.len() >= MAX_LSP_OPERATION_RESULTS + { + if let Some(evicted) = + this.lsp_operation_results.keys().next().cloned() + { + this.lsp_operation_results.remove(&evicted); + } + } + this.lsp_operation_results.insert(key, result); + } + if provider == lsp::JAVA_PROVIDER_ID { + if let Some(state) = java_state.clone() { + this.java_lsp_state = state; + if this.java_lsp_state == "ready" { + this.java_lsp_error = None; + } + } + this.java_lsp_project_preparation = preparation; + } if finished { + let java_failed = provider == lsp::JAVA_PROVIDER_ID + && java_state.as_deref() == Some("failed"); this.lsp_forget_session(&provider, &session_key, cx); + if java_failed { + this.java_lsp_state = "failed".to_string(); + this.java_lsp_error = Some( + "Java language-server session reported a terminal failure." + .to_string(), + ); + } } + cx.notify(); }); finished } - Err(_) => true, + Err(error) => { + let provider = provider_id.clone(); + let session_key = session.clone(); + let _ = this.update(cx, |this, cx| { + if this.lsp_workspace_generation != generation + || this.lsp_sessions.get(&provider).map(String::as_str) + != Some(session_key.as_str()) + { + return; + } + this.lsp_forget_session(&provider, &session_key, cx); + this.lsp_push_error(&provider, &error, cx); + }); + true + } }; if finished { + let _ = client + .execute::<_, serde_json::Value>( + &cx, + "lsp.destroyServer", + serde_json::json!({ "sessionId": session }), + ) + .await; break; } cx.background_executor() @@ -1361,7 +1728,9 @@ impl WorkbenchView { .await; } let _ = this.update(cx, |this, _cx| { - this.lsp_polling.remove(&session); + if this.lsp_workspace_generation == generation { + this.lsp_polling.remove(&session); + } }); }) .detach(); @@ -1369,11 +1738,18 @@ impl WorkbenchView { /// 会话终止后的清理:移除会话、轮询标记、文档版本与错误占位。 fn lsp_forget_session(&mut self, provider_id: &str, session: &str, cx: &mut Context) { + if self.lsp_sessions.get(provider_id).map(String::as_str) != Some(session) { + return; + } self.lsp_sessions.remove(provider_id); self.lsp_polling.remove(session); let prefix = format!("{session}\u{0}"); self.lsp_document_versions .retain(|key, _| !key.starts_with(&prefix)); + if provider_id == lsp::JAVA_PROVIDER_ID { + self.java_lsp_state = "stopped".to_string(); + self.java_lsp_project_preparation = None; + } let error_key = format!("__lsp__{provider_id}"); if self.lsp_diagnostics.remove(&error_key).is_some() { self.sync_diagnostics_panel(cx); @@ -1402,6 +1778,10 @@ impl WorkbenchView { /// 启动/同步失败时在诊断面板留一条错误记录,便于定位缺失的服务器。 fn lsp_push_error(&mut self, provider_id: &str, message: &str, cx: &mut Context) { + if provider_id == lsp::JAVA_PROVIDER_ID { + self.java_lsp_state = "failed".to_string(); + self.java_lsp_error = Some(message.to_string()); + } let entry = DiagnosticEntry { severity: "error".to_string(), file_path: provider_id.to_string(), From b566790ac6c834581e9a373211c5c6a555175e2b Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 17:34:08 +0800 Subject: [PATCH 58/59] =?UTF-8?q?feat(linux):=20=E7=BB=88=E7=AB=AF?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=A4=8D=E7=94=A8=20alacritty=5Fterminal=20?= =?UTF-8?q?=E5=BC=95=E6=93=8E=E5=B9=B6=E8=A1=A5=E9=BD=90=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重写 linux/src/workbench/terminal.rs,去掉自绘的固定 120x30 终端,改为 只做「上游引擎 -> GPUI 元素」的投影,终端语义全部交给 alacritty_terminal。 功能点: - 随窗缩放:容器尺寸变化时 resize 网格并同步 PTY 行列数,全屏 TUI 能拿到 正确窗口大小;上游 reflow 由 Term::resize 完成。 - 回滚滚动:滚轮驱动 Term::scroll_display,可查看历史输出。 - 鼠标选择:左键单击/双击/三击分别是 simple/semantic/lines 选区,拖动更新; 选区命中换算叠加回滚偏移,上滚后仍命中同一行。 - 复制粘贴:Ctrl+Shift+C/V 走 Term::selection_to_string 与系统剪贴板。 - 终端内搜索:Ctrl+Shift+F 打开搜索栏,用 term::search::RegexSearch + Term::search_next 上/下一个命中,命中滚入视口并复用 selection 高亮。 - 等宽对齐:单元格宽用上游 em_layout_width 按字号实测,鼠标命中所用 CellMetrics、网格 cols/rows、PTY 尺寸三者共用同一来源。 - 光标:聚焦时反色块,含行尾之后与空行两处补块。 其余: - i18n 新增 terminal.searchPlaceholder 文案键。 - 新增 Agent Note 记录「GPUI 无可复用完整终端控件、只复用引擎层」的边界。 - 新增 12 个测试:坐标/内边距/回滚偏移换算、256 色与加粗解算,以及引擎 联通性(解析入网格、display_offset、selection 导出、resize 列数)。 验证:cargo build/test 通过(lib 52 项);verify-test-stability 通过; verify-agent-notes 通过(36 篇)。 --- ...-09-25-linux-gpui-terminal-engine-reuse.md | 111 ++ linux/src/i18n.rs | 1 + linux/src/workbench/terminal.rs | 957 +++++++++++++++--- 3 files changed, 942 insertions(+), 127 deletions(-) create mode 100644 .agents/notes/implemented/architecture/2026-09-25-linux-gpui-terminal-engine-reuse.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/linux/src/i18n.rs b/linux/src/i18n.rs index 1750e59b3..821e5a585 100644 --- a/linux/src/i18n.rs +++ b/linux/src/i18n.rs @@ -166,6 +166,7 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { ), "terminal.cleared" => ("终端已清空。", "Terminal cleared."), "terminal.unavailable" => ("PTY 不可用", "PTY unavailable"), + "terminal.searchPlaceholder" => ("在终端中查找", "Find in terminal"), "workbench.settings" => ("设置", "Settings"), "workbench.sourceControl" => ("源代码管理", "Source Control"), "fileExplorer.searchFiles" => ("搜索文件", "Search files"), diff --git a/linux/src/workbench/terminal.rs b/linux/src/workbench/terminal.rs index 471b03b72..90b3f51a3 100644 --- a/linux/src/workbench/terminal.rs +++ b/linux/src/workbench/terminal.rs @@ -1,36 +1,61 @@ -//! Linux 原生 PTY 终端:`portable-pty` 跑 shell,`alacritty_terminal` -//!(Alacritty 网格)做真机解析与状态,键盘字符模式直输(对齐 IDEA -//! 内嵌终端的交互:历史、补全、方向键、中断均由 PTY 行规程处理)。 +//! Linux 原生 PTY 终端:`portable-pty` 跑 shell,`alacritty_terminal`(Alacritty +//! 网格引擎)做解析与状态,键盘字符模式直输(对齐 IDEA 内嵌终端的交互:历史、 +//! 补全、方向键、中断均由 PTY 行规程处理)。 //! -//! 渲染取网格可见区,同样式字符合并展示;聚焦时在光标处画反色块。 -//! 适配声明:多标签、新终端、终端内搜索、复制粘贴、滚到末尾跟随、 -//! PTY 随窗缩放暂不支持(缺选择/滚动控制,采固定 120x30 跟随显示)。 +//! 渲染层不自行实现终端语义,只做网格到 GPUI 元素的投影:可见区按固定字符单元格 +//! 排版,同样式字符合并为一段。选择、回滚、搜索、随窗缩放全部调用 +//! `alacritty_terminal` 的既有 API(`Term::scroll_display`、`Selection`、 +//! `RegexSearch`、`Term::resize` 与网格 reflow),不重复实现上游能力。 +//! +//! 适配声明:多标签、横向分屏、超链接点击、图形协议(Sixel/Kitty)暂不支持。 +//! +//! Note: 引擎复用边界与禁止手写终端语义的原因见 +//! `.agents/notes/implemented/architecture/2026-09-25-linux-gpui-terminal-engine-reuse.md`。 use alacritty_terminal::event::VoidListener; -use alacritty_terminal::grid::Dimensions; +use alacritty_terminal::grid::{Dimensions, Scroll}; +use alacritty_terminal::index::{Column, Direction, Line, Point as GridPoint, Side}; +use alacritty_terminal::selection::{Selection, SelectionType}; use alacritty_terminal::term::cell::Flags; +use alacritty_terminal::term::search::{Match, RegexSearch}; use alacritty_terminal::term::{Config, Term, TermMode}; -use alacritty_terminal::vte::ansi::{Color as AlacColor, NamedColor, Processor, Rgb}; -use gpui_kit::component::scroll::ScrollableElement as _; -use gpui_kit::component::{h_flex, v_flex}; +use alacritty_terminal::vte::ansi::{Color as AlacColor, CursorShape, NamedColor, Processor, Rgb}; +use gpui_kit::component::button::{Button, ButtonVariants as _}; +use gpui_kit::component::input::{Input, InputEvent, InputState}; +use gpui_kit::component::{h_flex, v_flex, Sizable as _}; use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::{ - div, px, Context, FocusHandle, FontWeight, InteractiveElement as _, IntoElement, KeyDownEvent, - ParentElement as _, Render, Rgba, StatefulInteractiveElement as _, Styled as _, Window, + div, px, App, AppContext as _, Bounds, ClipboardItem, Context, FocusHandle, FontWeight, + InteractiveElement as _, IntoElement, KeyDownEvent, MouseButton, MouseDownEvent, + MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Render, Rgba, ScrollDelta, + ScrollWheelEvent, StatefulInteractiveElement as _, Styled as _, Window, }; use portable_pty::{native_pty_system, CommandBuilder, PtySize}; +use std::cell::Cell; use std::io::{Read, Write}; +use std::rc::Rc; use std::sync::{mpsc, Arc, Mutex}; use std::thread; use crate::settings; use crate::theme::ThemeColors; -/// PTY 固定尺寸(随窗缩放暂不支持)。 -const TERMINAL_COLS: usize = 120; -const TERMINAL_ROWS: usize = 30; +/// 初始网格尺寸(首帧布局完成前使用,随后按容器实测尺寸 resize)。 +const INITIAL_COLS: usize = 80; +const INITIAL_ROWS: usize = 24; + +/// 网格最小尺寸,避免退化到 0 列触发上游断言。 +const MIN_COLS: usize = 2; +const MIN_ROWS: usize = 1; + +/// 行高相对字号的倍率(等宽终端常用 1.4)。 +const LINE_HEIGHT_RATIO: f32 = 1.4; + +/// 网格内容内边距(左上角偏移,鼠标命中换算需扣除)。 +const GRID_PADDING: f32 = 4.0; /// 网格尺寸(`Term::new` 与 `resize` 共用)。 +#[derive(Debug, Clone, Copy)] struct TermDims { cols: usize, rows: usize, @@ -119,6 +144,34 @@ fn resolve_color(color: AlacColor, bold: bool, default: Rgba) -> Rgba { } } +/// 容器内局部坐标(已扣除容器原点)→ 网格点。 +/// +/// 先扣除网格内边距,再按单元格尺寸取整;越界返回 `None`。视口行需叠加 +/// 回滚偏移才是网格行(上游 `viewport_to_point` 语义),这样上滚后鼠标 +/// 选择仍命中用户看到的同一行。 +fn viewport_to_grid_point( + local_x: f32, + local_y: f32, + cell: CellMetrics, + size: TermDims, + display_offset: usize, +) -> Option { + let local_x = local_x - GRID_PADDING; + let local_y = local_y - GRID_PADDING; + if local_x < 0.0 || local_y < 0.0 { + return None; + } + + let col = (local_x / cell.width).floor() as usize; + let viewport_line = (local_y / cell.height).floor() as usize; + if col >= size.cols || viewport_line >= size.rows { + return None; + } + + let line = Line(viewport_line as i32 - display_offset as i32); + Some(GridPoint::new(line, Column(col))) +} + /// Linux 原生 PTY 会话 pub struct TerminalSession { writer: Arc>>, @@ -194,8 +247,7 @@ impl TerminalSession { Ok(()) } - /// PTY 缩放(随窗缩放接入后调用,当前固定尺寸保留接口)。 - #[allow(dead_code)] + /// PTY 缩放:容器尺寸变化时调用,使 shell / 全屏 TUI 拿到最新行列数。 pub fn resize(&self, cols: u16, rows: u16) { if let Ok(master) = self.master.lock() { let _ = master.resize(PtySize { @@ -208,30 +260,102 @@ impl TerminalSession { } } +/// 单元格尺寸(像素),渲染与鼠标命中都用它换算行列。 +#[derive(Debug, Clone, Copy)] +struct CellMetrics { + width: f32, + height: f32, +} + +/// 终端内搜索状态:查询串、上游正则缓存与命中计数。 +struct SearchState { + query: String, + regex: Option, + /// 当前命中序号(1 起,0 表示无命中)。 + current: usize, + /// 命中总数;上游按需搜索,不预先全量统计,未统计时为 0。 + total: usize, +} + +impl SearchState { + fn new() -> Self { + Self { + query: String::new(), + regex: None, + current: 0, + total: 0, + } + } + + fn clear(&mut self) { + self.query.clear(); + self.regex = None; + self.current = 0; + self.total = 0; + } +} + /// 终端视图组件:点击聚焦后键盘字符模式直输(IDEA 式交互)。 +/// +/// 网格引擎与交互能力全部来自上游 `alacritty_terminal`:本结构只负责调用 +/// 它的 resize/scroll/selection/search API,并把结果投影成 GPUI 元素。 pub struct TerminalView { pub session: Option, pub working_dir: String, term: Term, processor: Processor, focus_handle: FocusHandle, + /// 最近一次已知的网格尺寸(行列),用于判断是否需要 resize。 + size: TermDims, + /// 单元格像素尺寸;每次 render 按当前字号实测写入。 + cell: CellMetrics, + /// 网格内容区边界(窗口坐标);由 `on_children_prepainted` 写入。 + grid_bounds: Rc>>>, + /// 正在拖拽选择时的选区类型。 + dragging: Option, + /// 终端内搜索状态。 + search: SearchState, + /// 搜索栏输入框实体(懒创建)。 + search_input: Option>, + /// 搜索输入框 Change 订阅。 + search_subscription: Option, + /// 请求在下一帧展开搜索栏(键盘事件没有 `Window`,输入框需在 render 创建)。 + search_requested: bool, } impl TerminalView { pub fn new(working_dir: String, cx: &mut Context) -> Self { + let scrollback = settings::get(cx).terminal_scrollback.max(1); let mut view = Self { session: None, working_dir, term: Term::new( - Config::default(), + Config { + scrolling_history: scrollback, + ..Config::default() + }, &TermDims { - cols: TERMINAL_COLS, - rows: TERMINAL_ROWS, + cols: INITIAL_COLS, + rows: INITIAL_ROWS, }, VoidListener, ), processor: Processor::new(), focus_handle: cx.focus_handle(), + size: TermDims { + cols: INITIAL_COLS, + rows: INITIAL_ROWS, + }, + cell: CellMetrics { + width: 8.0, + height: 18.0, + }, + grid_bounds: Rc::new(Cell::new(None)), + dragging: None, + search: SearchState::new(), + search_input: None, + search_subscription: None, + search_requested: false, }; view.attach_session(cx); view @@ -241,8 +365,8 @@ impl TerminalView { fn attach_session(&mut self, cx: &mut Context) { let shell = Self::resolve_shell(cx); match TerminalSession::new( - TERMINAL_COLS as u16, - TERMINAL_ROWS as u16, + self.size.cols as u16, + self.size.rows as u16, &self.working_dir, &shell, ) { @@ -300,7 +424,7 @@ impl TerminalView { /// 解析生效 shell:设置 `terminalDefaultShellId` 非空即用(名称经 /// `PATH` 或 `/bin` 解析为路径),为空回退 `$SHELL`。 - fn resolve_shell(cx: &gpui_kit::App) -> String { + fn resolve_shell(cx: &App) -> String { let id = settings::get(cx) .terminal_default_shell_id .trim() @@ -347,18 +471,184 @@ impl TerminalView { /// 用现有工作目录重建 PTY 会话与网格(失败则置空并通知)。 pub fn respawn(&mut self, cx: &mut Context) { self.session = None; + let scrollback = settings::get(cx).terminal_scrollback.max(1); self.term = Term::new( - Config::default(), - &TermDims { - cols: TERMINAL_COLS, - rows: TERMINAL_ROWS, + Config { + scrolling_history: scrollback, + ..Config::default() }, + &self.size, VoidListener, ); self.processor = Processor::new(); + self.search.clear(); + self.dragging = None; self.attach_session(cx); } + /// 依据容器实测尺寸同步网格与 PTY(上游 reflow 由 `Term::resize` 完成)。 + fn sync_size(&mut self, bounds: Bounds, cx: &mut Context) { + let width = f32::from(bounds.size.width) - GRID_PADDING * 2.0; + let height = f32::from(bounds.size.height) - GRID_PADDING * 2.0; + if width <= 0.0 || height <= 0.0 { + return; + } + + let cols = ((width / self.cell.width).floor() as usize).max(MIN_COLS); + let rows = ((height / self.cell.height).floor() as usize).max(MIN_ROWS); + if cols == self.size.cols && rows == self.size.rows { + return; + } + + self.size = TermDims { cols, rows }; + self.term.resize(self.size); + if let Some(session) = &self.session { + session.resize(cols as u16, rows as u16); + } + cx.notify(); + } + + /// 窗口坐标 → 网格点(含回滚偏移);越界返回 `None`。 + fn grid_point_at(&self, position: gpui_kit::Point) -> Option { + let bounds = self.grid_bounds.get()?; + let display_offset = self.term.grid().display_offset(); + viewport_to_grid_point( + f32::from(position.x) - f32::from(bounds.origin.x), + f32::from(position.y) - f32::from(bounds.origin.y), + self.cell, + self.size, + display_offset, + ) + } + + /// 开始一次选择(单击 simple / 双击 semantic / 三击 lines)。 + fn begin_selection(&mut self, point: GridPoint, ty: SelectionType) { + self.term.selection = Some(Selection::new(ty, point, Side::Left)); + } + + /// 更新拖拽中的选择终点。 + fn update_selection(&mut self, point: GridPoint) { + if let Some(selection) = self.term.selection.as_mut() { + selection.update(point, Side::Right); + } + } + + /// 复制当前选择到系统剪贴板;无选择返回 false。 + fn copy_selection(&mut self, cx: &mut App) -> bool { + let Some(text) = self.term.selection_to_string() else { + return false; + }; + if text.is_empty() { + return false; + } + cx.write_to_clipboard(ClipboardItem::new_string(text)); + true + } + + /// 从系统剪贴板粘贴到 PTY(读不到或为空则忽略)。 + fn paste_clipboard(&mut self, cx: &mut App) { + let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) else { + return; + }; + if let Some(session) = &self.session { + let _ = session.write_bytes(text.as_bytes()); + } + } + + /// 搜索串变化:重建上游正则并跳到首个命中。 + fn set_search_query(&mut self, query: String, cx: &mut Context) { + self.search.query = query; + self.search.current = 0; + self.search.total = 0; + self.search.regex = if self.search.query.is_empty() { + None + } else { + RegexSearch::new(&self.search.query).ok() + }; + self.search_step(true, cx); + } + + /// 跳到下一个/上一个命中。`forward` 决定方向。 + fn search_step(&mut self, forward: bool, cx: &mut Context) { + let Some(regex) = self.search.regex.as_mut() else { + cx.notify(); + return; + }; + if self.search.query.is_empty() { + cx.notify(); + return; + } + + let direction = if forward { + Direction::Right + } else { + Direction::Left + }; + let origin = self.term.grid().cursor.point; + let found: Option = + self.term + .search_next(regex, origin, direction, Side::Left, None); + + if let Some(found) = found { + self.search.current = if forward { + self.search.current.saturating_add(1).max(1) + } else { + self.search.current.saturating_sub(1).max(1) + }; + // 把命中滚入视口并选中,复用上游 selection 作为高亮来源。 + let start = *found.start(); + let end = *found.end(); + self.term.scroll_to_point(start); + let mut selection = Selection::new(SelectionType::Simple, start, Side::Left); + selection.update(end, Side::Right); + self.term.selection = Some(selection); + } else { + self.search.current = 0; + } + cx.notify(); + } + + /// 关闭搜索栏并清除搜索态选择。 + fn close_search(&mut self, cx: &mut Context) { + self.search.clear(); + self.term.selection = None; + self.search_requested = false; + // 丢弃输入框实体即释放搜索栏;下次打开重建空值输入框。 + self.search_subscription = None; + self.search_input = None; + cx.notify(); + } + + /// 搜索栏是否可见。 + fn search_visible(&self) -> bool { + self.search_requested || self.search_input.is_some() + } + + /// 懒创建搜索输入框并订阅 Change 事件,值同步到搜索状态。 + fn ensure_search_input( + slot: &mut Option>, + subscription: &mut Option, + window: &mut Window, + cx: &mut Context, + ) -> gpui_kit::Entity { + if let Some(entity) = slot { + return entity.clone(); + } + let entity = cx.new(|cx| InputState::new(window, cx)); + let value_source = entity.clone(); + *subscription = Some(cx.subscribe( + &entity, + move |this: &mut Self, _, event: &InputEvent, cx| { + if matches!(event, InputEvent::Change) { + let value = value_source.read(cx).value().to_string(); + this.set_search_query(value, cx); + } + }, + )); + *slot = Some(entity.clone()); + entity + } + /// 应用光标键模式:程序用 `\x1bO` 系,否则 `\x1b[` 系。 fn cursor_key(&self, normal: &[u8], app: &[u8]) -> Vec { if self.term.mode().contains(TermMode::APP_CURSOR) { @@ -370,9 +660,32 @@ impl TerminalView { /// 键盘直输:可打印字符原样写 PTY,控制组合与功能键转义序列, /// 未知键放行(IDEA 快捷键不断)。返回是否消费。 - fn handle_key(&mut self, event: &KeyDownEvent) -> bool { + fn handle_key(&mut self, event: &KeyDownEvent, cx: &mut App) -> bool { let key = event.keystroke.key.as_str(); let mods = &event.keystroke.modifiers; + + // 终端内复制/粘贴/搜索:Ctrl+Shift+C / Ctrl+Shift+V / Ctrl+Shift+F。 + // 放在最前,避免被下面的 Ctrl 控制字符分支吞掉。 + if mods.control && mods.shift && !mods.alt && !mods.platform { + match key.to_lowercase().as_str() { + "c" => { + self.copy_selection(cx); + return true; + } + "v" => { + self.paste_clipboard(cx); + return true; + } + "f" => { + // 输入框需要 `Window`,此处只登记请求,render 阶段创建。 + // 刷新由调用方 `on_key_down` 统一触发。 + self.search_requested = true; + return true; + } + _ => {} + } + } + let bytes: Vec = if mods.control && !mods.alt && !mods.platform { // Ctrl+字母 → 控制字符(Ctrl+C 中断等)。 let lower = key.to_lowercase(); @@ -443,15 +756,38 @@ impl TerminalView { impl Render for TerminalView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let focused = self.focus_handle.is_focused(window); + + // 单元格基准宽度:等宽字体下用上游 `em_layout_width` 实测(随字号变化)。 + let font_size = px(crate::settings::get(cx).terminal_font_size); + let font = gpui_kit::Font { + family: "monospace".into(), + ..Default::default() + }; + let font_id = window.text_system().resolve_font(&font); + let cell_width = + f32::from(window.text_system().em_layout_width(font_id, font_size)).max(1.0); + let line_height = f32::from(font_size) * LINE_HEIGHT_RATIO; + self.cell = CellMetrics { + width: cell_width, + height: line_height, + }; + + // 用上一帧 prepaint 记录的容器边界同步网格与 PTY;尺寸变化时 + // `sync_size` 内部 resize 上游网格并通知 PTY。 + if let Some(bounds) = self.grid_bounds.get() { + self.sync_size(bounds, cx); + } + + // 可见区按行分组(`display_iter` 即当前视口,已含回滚偏移)。 + let content = self.term.renderable_content(); + let selection = content.selection; let default_fg = ThemeColors::foreground(); let default_bg = ThemeColors::background(); + let selection_color = ThemeColors::accent(); - // 可见区按行分组(`display_iter` 即当前视口)。 - let content = self.term.renderable_content(); - let mut rows: Vec<(i32, Vec<(usize, char, AlacColor, AlacColor, bool)>)> = Vec::new(); + let mut rows: Vec<(i32, Vec)> = Vec::new(); for indexed in content.display_iter { let line = indexed.point.line.0; - let col = indexed.point.column.0; if rows.last().map(|(number, _)| *number) != Some(line) { rows.push((line, Vec::new())); } @@ -462,21 +798,76 @@ impl Render for TerminalView { std::mem::swap(&mut fg, &mut bg); } let bold = cell.flags.contains(Flags::BOLD); + let selected = selection + .as_ref() + .is_some_and(|range| range.contains(indexed.point)); rows.last_mut() .expect("rows non-empty after push") .1 - .push((col, cell.c, fg, bg, bold)); + .push(TermCell { + c: cell.c, + fg, + bg, + bold, + selected, + }); } let (cursor_line, cursor_col, cursor_visible) = ( content.cursor.point.line.0, content.cursor.point.column.0, focused - && !matches!( - content.cursor.shape, - alacritty_terminal::vte::ansi::CursorShape::Hidden - ), + && !matches!(content.cursor.shape, CursorShape::Hidden) + && self.term.grid().display_offset() == 0, ); + let grid = div() + .flex_1() + .w_full() + .min_h_0() + .overflow_hidden() + .pt(px(GRID_PADDING)) + .pl(px(GRID_PADDING)) + .text_size(font_size) + .font_family("monospace") + .children(rows.into_iter().map(|(line, cells)| { + render_term_row( + line, + &cells, + cursor_visible && line == cursor_line, + cursor_col, + default_fg, + default_bg, + selection_color, + cell_width, + line_height, + ) + })); + + // 搜索输入框懒创建 + 一次性订阅 Change(键盘请求在 render 落地)。 + let search_input = self.search_visible().then(|| { + Self::ensure_search_input( + &mut self.search_input, + &mut self.search_subscription, + window, + cx, + ) + }); + + // prepaint 记录网格容器边界,供鼠标坐标换算与 resize。 + // `on_children_prepainted` 给出的是子元素边界,因此外层包一层只有一个 + // 子元素的容器,`bounds.first()` 就是网格容器自身的实测边界。 + let bounds_slot = self.grid_bounds.clone(); + let grid_with_bounds = div() + .flex_1() + .w_full() + .min_h_0() + .on_children_prepainted(move |bounds, _window, _cx| { + if let Some(first) = bounds.first() { + bounds_slot.set(Some(*first)); + } + }) + .child(grid); + v_flex() .size_full() .bg(ThemeColors::background()) @@ -487,131 +878,443 @@ impl Render for TerminalView { window.focus(&this.focus_handle, cx); })) .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { - if this.handle_key(event) { + if this.handle_key(event, cx) { cx.stop_propagation(); cx.notify(); } })) + // 鼠标:按下定位并记录选区起点,拖动更新,抬起结束。 + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, event: &MouseDownEvent, _window, cx| { + let Some(point) = this.grid_point_at(event.position) else { + return; + }; + let ty = match event.click_count { + 0 | 1 => SelectionType::Simple, + 2 => SelectionType::Semantic, + _ => SelectionType::Lines, + }; + this.begin_selection(point, ty); + this.dragging = Some(ty); + cx.notify(); + }), + ) + .on_mouse_move(cx.listener(|this, event: &MouseMoveEvent, _window, cx| { + if this.dragging.is_none() { + return; + } + if let Some(point) = this.grid_point_at(event.position) { + this.update_selection(point); + cx.notify(); + } + })) + .on_mouse_up( + MouseButton::Left, + cx.listener(|this, _event: &MouseUpEvent, _window, cx| { + this.dragging = None; + cx.notify(); + }), + ) + // 滚轮:直接驱动上游滚动缓冲。 + .on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _window, cx| { + let lines = match event.delta { + ScrollDelta::Lines(point) => f64::from(point.y), + ScrollDelta::Pixels(point) => f64::from(point.y) / this.cell.height as f64, + }; + let delta = lines.round() as i32; + if delta != 0 { + this.term.scroll_display(Scroll::Delta(delta)); + cx.notify(); + } + })) + .when_some(search_input, |el, input| { + el.child(self.render_search_bar(input, cx)) + }) + .child(grid_with_bounds) + } +} + +impl TerminalView { + /// 渲染搜索栏(终端内搜索,对齐 Tauri 终端搜索条)。 + fn render_search_bar( + &self, + input: gpui_kit::Entity, + cx: &mut Context, + ) -> impl IntoElement { + let counter = if self.search.query.is_empty() { + String::new() + } else { + format!( + "{}/{}", + self.search.current, + self.search.total.max(self.search.current) + ) + }; + h_flex() + .w_full() + .flex_shrink_0() + .items_center() + .gap_2() + .px_2() + .py_1() + .bg(ThemeColors::bg_tab_bar()) + .border_b_1() + .border_color(ThemeColors::border()) .child( - // 输出网格(控制栏由底部窗格头负责,不再自带)。 div() .flex_1() - .w_full() - .min_h_0() - .overflow_y_scrollbar() - .p_2() + .child(Input::new(&input).small().cleanable(true)), + ) + .child( + div() .text_xs() - .text_size(px(crate::settings::get(cx).terminal_font_size)) - .font_family("monospace") - .children(rows.into_iter().map(|(line, cells)| { - render_term_row( - line, - &cells, - cursor_visible && line == cursor_line, - cursor_col, - default_fg, - default_bg, - ) + .text_color(ThemeColors::text_muted()) + .child(counter), + ) + .child( + Button::new("terminal-search-prev") + .small() + .ghost() + .icon(gpui_kit::assets::IconName::ChevronUp) + .on_click(cx.listener(|this, _event, _window, cx| { + this.search_step(false, cx); + })), + ) + .child( + Button::new("terminal-search-next") + .small() + .ghost() + .icon(gpui_kit::assets::IconName::ChevronDown) + .on_click(cx.listener(|this, _event, _window, cx| { + this.search_step(true, cx); + })), + ) + .child( + Button::new("terminal-search-close") + .small() + .ghost() + .icon(gpui_kit::assets::IconName::X) + .on_click(cx.listener(|this, _event, _window, cx| { + this.close_search(cx); })), ) } } -/// 渲染网格一行:同样式字符合并为一段;聚焦时在光标处画反色块。 +/// 一个网格单元格的渲染数据(有序遍历 `display_iter` 时收集)。 +#[derive(Clone, Copy)] +struct TermCell { + c: char, + fg: AlacColor, + bg: AlacColor, + bold: bool, + selected: bool, +} + +/// 渲染网格一行:同样式字符合并为一段;选中字符用选区底色;聚焦时在光标处 +/// 画反色块。每段用固定像素宽度(`字符数 × cell_width`)保证等宽对齐, +/// 鼠标命中换算依赖同一套单元格尺寸。 +#[allow(clippy::too_many_arguments)] fn render_term_row( line: i32, - cells: &[(usize, char, AlacColor, AlacColor, bool)], + cells: &[TermCell], cursor_here: bool, cursor_col: usize, default_fg: Rgba, default_bg: Rgba, + selection_color: Rgba, + cell_width: f32, + line_height: f32, ) -> gpui_kit::AnyElement { - // 合并同样式段(宽字符占位格视为空格参与合并)。 - let mut spans: Vec<(AlacColor, AlacColor, bool, String)> = Vec::new(); - for (col, c, fg, bg, bold) in cells { - let _ = col; - let key = (*fg, *bg, *bold); - if let Some((sfg, sbg, sbold, text)) = spans.last_mut() { - if (*sfg, *sbg, *sbold) == key { - text.push(*c); - continue; - } - } - spans.push((*fg, *bg, *bold, c.to_string())); - } - let total: usize = spans - .iter() - .map(|(_, _, _, text)| text.chars().count()) - .sum(); - // 光标超出文本末尾时在行尾补空格块。 - let trailing = cursor_here && cursor_col >= total; - let mut row_el = h_flex() + let mut row = h_flex() .id(format!("term-row-{line}")) .w_full() + .h(px(line_height)) .items_center() - .flex_wrap() - .text_xs() .font_family("monospace"); - if spans.is_empty() && !trailing { - return row_el - .child(div().child(" ".to_string())) - .into_any_element(); - } - let mut col = 0; - let mut cursor_drawn = false; - for (fg, bg, bold, text) in &spans { - let fg_resolved = resolve_color(*fg, *bold, default_fg); - let bg_resolved = resolve_color(*bg, false, default_bg); - let chunk = |text: String| { - div() - .text_color(fg_resolved) - .when(*bg != AlacColor::Named(NamedColor::Background), |el| { - el.bg(bg_resolved) - }) - .when(*bold, |el| el.font_weight(FontWeight::BOLD)) - .child(text) - }; - let span_len = text.chars().count(); - // 光标块整行只画一次:完全在光标前/后的段原样输出。 - if trailing || col + span_len <= cursor_col || cursor_drawn { - row_el = row_el.child(chunk(text.clone())); - col += span_len; + if cells.is_empty() { + // 空行:只有光标落在首格时画块,否则保底一个空格占位。 + if cursor_here && cursor_col == 0 { + row = row.child( + div() + .flex_shrink_0() + .w(px(cell_width)) + .text_color(default_bg) + .bg(default_fg) + .child(" ".to_string()), + ); + } else { + row = row.child(div().child(" ")); + } + return row.into_any_element(); + } + + // 逐格构建:同样式连续字符合并成段,光标位单独成块。 + let mut index = 0usize; + while index < cells.len() { + let cell = cells[index]; + let is_cursor = cursor_here && index == cursor_col; + + if is_cursor { + // 光标块:反色(前景当底、背景当字),宽一格。 + row = row.child( + div() + .flex_shrink_0() + .w(px(cell_width)) + .text_color(default_bg) + .bg(resolve_color(cell.fg, cell.bold, default_fg)) + .child(cell.c.to_string()), + ); + index += 1; continue; } - let mut before = String::new(); - let mut cursor_char: Option = None; - let mut after = String::new(); - for c in text.chars() { - if col < cursor_col { - before.push(c); - } else if cursor_char.is_none() { - cursor_char = Some(c); - } else { - after.push(c); + + // 合并同 (fg,bg,bold,selected) 的后续字符。 + let mut text = String::new(); + text.push(cell.c); + let mut len = 1usize; + while index + len < cells.len() { + let next = cells[index + len]; + if cursor_here && index + len == cursor_col { + break; } - col += 1; - } - if !before.is_empty() { - row_el = row_el.child(chunk(before)); + if next.fg != cell.fg + || next.bg != cell.bg + || next.bold != cell.bold + || next.selected != cell.selected + { + break; + } + text.push(next.c); + len += 1; } - row_el = row_el.child( + + let fg = resolve_color(cell.fg, cell.bold, default_fg); + let bg = resolve_color(cell.bg, false, default_bg); + row = row.child( div() - .text_color(default_bg) - .bg(fg_resolved) - .child(cursor_char.unwrap_or(' ').to_string()), + .flex_shrink_0() + .w(px(cell_width * len as f32)) + .text_color(fg) + .when(cell.selected, |el| el.bg(selection_color)) + .when( + !cell.selected && cell.bg != AlacColor::Named(NamedColor::Background), + |el| el.bg(bg), + ) + .when(cell.bold, |el| el.font_weight(FontWeight::BOLD)) + .child(text), ); - cursor_drawn = true; - if !after.is_empty() { - row_el = row_el.child(chunk(after)); - } + index += len; } - if trailing { - row_el = row_el.child( + + // 光标超出已给出单元格(行尾之后):补一个反色空格块。 + if cursor_here && cursor_col >= cells.len() { + row = row.child( div() + .flex_shrink_0() + .w(px(cell_width)) .text_color(default_bg) .bg(default_fg) .child(" ".to_string()), ); } - row_el.into_any_element() + + row.into_any_element() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn dims(cols: usize, rows: usize) -> TermDims { + TermDims { cols, rows } + } + + fn cell() -> CellMetrics { + CellMetrics { + width: 8.0, + height: 16.0, + } + } + + /// 网格左上角第一格:局部坐标需扣除内边距。 + #[test] + fn maps_top_left_corner_to_origin_cell() { + let point = viewport_to_grid_point(GRID_PADDING, GRID_PADDING, cell(), dims(80, 24), 0) + .expect("top-left must hit a cell"); + assert_eq!(point.line.0, 0); + assert_eq!(point.column.0, 0); + } + + /// 内边距内的坐标不算命中,避免选中出现偏移半格。 + #[test] + fn rejects_point_inside_padding() { + assert!( + viewport_to_grid_point(GRID_PADDING - 0.5, GRID_PADDING, cell(), dims(80, 24), 0) + .is_none() + ); + assert!( + viewport_to_grid_point(GRID_PADDING, GRID_PADDING - 0.5, cell(), dims(80, 24), 0) + .is_none() + ); + } + + /// 列/行按单元格尺寸取整:第 n 格覆盖 [n*w, (n+1)*w)。 + #[test] + fn rounds_down_to_containing_cell() { + let point = viewport_to_grid_point( + GRID_PADDING + cell().width * 3.0 + 1.0, + GRID_PADDING + cell().height * 2.0 + 1.0, + cell(), + dims(80, 24), + 0, + ) + .expect("inside grid"); + assert_eq!(point.column.0, 3); + assert_eq!(point.line.0, 2); + } + + /// 越出网格列/行的坐标不命中。 + #[test] + fn rejects_out_of_bounds() { + let cols = 10; + let rows = 5; + let beyond_cols = viewport_to_grid_point( + GRID_PADDING + cell().width * cols as f32, + GRID_PADDING, + cell(), + dims(cols, rows), + 0, + ); + assert!(beyond_cols.is_none()); + let beyond_rows = viewport_to_grid_point( + GRID_PADDING, + GRID_PADDING + cell().height * rows as f32, + cell(), + dims(cols, rows), + 0, + ); + assert!(beyond_rows.is_none()); + } + + /// 回滚偏移把视口行还原成网格行:上滚 3 行后,视口首行是网格 -3 行。 + #[test] + fn applies_scrollback_offset_to_viewport_line() { + let point = viewport_to_grid_point(GRID_PADDING, GRID_PADDING, cell(), dims(80, 24), 3) + .expect("top-left with scrollback"); + assert_eq!(point.line.0, -3); + assert_eq!(point.column.0, 0); + } + + /// 256 色查表:0-15 取基础色,16 起为立方体,232 起为灰阶。 + #[test] + fn palette_256_covers_base_cube_and_grayscale() { + assert_eq!(palette_256(0), ANSI_PALETTE[0]); + assert_eq!(palette_256(15), ANSI_PALETTE[15]); + assert_eq!(palette_256(16), (0, 0, 0)); + assert_eq!(palette_256(231), (255, 255, 255)); + assert_eq!(palette_256(232), (8, 8, 8)); + assert_eq!(palette_256(255), (238, 238, 238)); + } + + /// 加粗时 0-7 基础色提升到 8-15 高亮 variant(标准终端行为)。 + #[test] + fn bold_promotes_base_colors_to_bright_variants() { + let bold = resolve_color(AlacColor::Named(NamedColor::Red), true, Rgba::default()); + let normal = resolve_color(AlacColor::Named(NamedColor::Red), false, Rgba::default()); + assert_eq!(bold, rgba_of(ANSI_PALETTE[9])); + assert_eq!(normal, rgba_of(ANSI_PALETTE[1])); + assert_ne!(bold, normal); + } + + /// 引擎联通性:喂入带 ANSI 换行的字节后,可见区出现对应文本。 + /// 这条守住 render 路径依赖的上游解析/网格行为。 + #[test] + fn engine_parses_output_into_grid() { + let mut term = Term::new(Config::default(), &dims(20, 4), VoidListener); + let mut processor: Processor = Processor::new(); + processor.advance(&mut term, b"hello\r\nworld"); + let content = term.renderable_content(); + let text: String = content.display_iter.map(|indexed| indexed.cell.c).collect(); + assert!( + text.contains("hello"), + "first line must contain hello: {text:?}" + ); + assert!( + text.contains("world"), + "second line must contain world: {text:?}" + ); + } + + /// 引擎联通性:写入超过屏高的行后可用上游 API 滚出显示偏移。 + #[test] + fn engine_scrollback_exposes_display_offset() { + let mut term = Term::new( + Config { + scrolling_history: 100, + ..Config::default() + }, + &dims(20, 2), + VoidListener, + ); + let mut processor: Processor = Processor::new(); + processor.advance(&mut term, b"one\r\ntwo\r\nthree\r\nfour"); + assert_eq!(term.grid().display_offset(), 0); + term.scroll_display(Scroll::Delta(2)); + assert_eq!(term.grid().display_offset(), 2); + term.scroll_display(Scroll::Bottom); + assert_eq!(term.grid().display_offset(), 0); + } + + /// 引擎联通性:选择一块文本后可经上游导出字符串(复制链路)。 + #[test] + fn engine_selection_to_string_roundtrips() { + let mut term = Term::new(Config::default(), &dims(20, 4), VoidListener); + let mut processor: Processor = Processor::new(); + processor.advance(&mut term, b"abcdef"); + let mut selection = Selection::new( + SelectionType::Simple, + GridPoint::new(Line(0), Column(0)), + Side::Left, + ); + selection.update(GridPoint::new(Line(0), Column(2)), Side::Right); + term.selection = Some(selection); + let text = term.selection_to_string().expect("selection must export"); + assert!(text.starts_with("abc"), "selection text: {text:?}"); + } + + /// 引擎联通性:resize 改变列数(随窗缩放依赖上游 reflow)。 + #[test] + fn engine_resize_updates_columns() { + use alacritty_terminal::grid::Dimensions as _; + let mut term = Term::new(Config::default(), &dims(80, 24), VoidListener); + assert_eq!(term.columns(), 80); + term.resize(dims(100, 30)); + assert_eq!(term.columns(), 100); + assert_eq!(term.screen_lines(), 30); + } + + /// 默认前景/背景取主题色;加粗不改变已高于 7 的索引颜色。 + #[test] + fn default_colors_follow_theme_and_bold_ignored_above_index_seven() { + let theme = Rgba { + r: 0.25, + g: 0.5, + b: 0.75, + a: 1.0, + }; + assert_eq!( + resolve_color(AlacColor::Named(NamedColor::Foreground), false, theme), + theme + ); + assert_eq!( + resolve_color(AlacColor::Named(NamedColor::Background), true, theme), + theme + ); + assert_eq!( + resolve_color(AlacColor::Indexed(9), true, theme), + rgba_of(ANSI_PALETTE[9]) + ); + } } From 4c54ecaf6d11e6ecb603238052b8df193086d654 Mon Sep 17 00:00:00 2001 From: chenjunjie Date: Fri, 25 Sep 2026 17:34:27 +0800 Subject: [PATCH 59/59] =?UTF-8?q?feat(linux):=20=E5=8E=BB=E6=8E=89?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E9=87=8C=E7=9A=84=E9=85=8D=E8=89=B2=E4=B8=BB?= =?UTF-8?q?=E9=A2=98=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 设置 → 常规 → 外观 里同时有「配色主题」和「外观模式」两行,但 Linux 只有 lithe-dark / lithe-light 两个主题,前者与后者的浅色/深色选项完全重复, 且「外观模式」已额外覆盖「跟随系统」,因此删掉冗余的「配色主题」行。 - `settings_dialog.rs`:移除 `general-theme` 下拉行(含其 commit_theme 分支逻辑,该逻辑已由外观模式行覆盖)。 - `i18n.rs`:删除不再使用的 `settings.mac.colorTheme` 文案; `settings.mac.appearanceDescription` 改为只描述外观模式 (原文案「选择配色主题,并设置是否跟随系统外观」已不适用)。 验证:`cargo check` 无警告;`cargo test --lib` 52 通过。 --- linux/src/i18n.rs | 6 ++-- linux/src/workbench/settings_dialog.rs | 38 -------------------------- 2 files changed, 2 insertions(+), 42 deletions(-) diff --git a/linux/src/i18n.rs b/linux/src/i18n.rs index 821e5a585..38b478628 100644 --- a/linux/src/i18n.rs +++ b/linux/src/i18n.rs @@ -166,7 +166,6 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { ), "terminal.cleared" => ("终端已清空。", "Terminal cleared."), "terminal.unavailable" => ("PTY 不可用", "PTY unavailable"), - "terminal.searchPlaceholder" => ("在终端中查找", "Find in terminal"), "workbench.settings" => ("设置", "Settings"), "workbench.sourceControl" => ("源代码管理", "Source Control"), "fileExplorer.searchFiles" => ("搜索文件", "Search files"), @@ -355,11 +354,10 @@ pub fn menu_text(cx: &App, key: &str) -> &'static str { "ui.save" => ("保存", "Save"), // ---- 设置对话框内容:常规(与 Tauri macos-settings-panels.tsx 对齐) ---- "settings.mac.appearance" => ("外观", "Appearance"), - "settings.mac.colorTheme" => ("配色主题", "Color theme"), "settings.mac.appearanceMode" => ("外观模式", "Appearance mode"), "settings.mac.appearanceDescription" => ( - "选择配色主题,并设置是否跟随系统外观。", - "Choose a color theme and whether Lithe follows the system appearance.", + "选择界面外观,可固定为浅色或深色,或跟随系统。", + "Choose the interface appearance: light, dark, or follow the system.", ), "settings.mac.followSystem" => ("跟随系统", "Follow System"), "settings.mac.light" => ("浅色", "Light"), diff --git a/linux/src/workbench/settings_dialog.rs b/linux/src/workbench/settings_dialog.rs index 08edadfaa..7d2d1c5c1 100644 --- a/linux/src/workbench/settings_dialog.rs +++ b/linux/src/workbench/settings_dialog.rs @@ -981,47 +981,9 @@ impl SettingsDialog { v_flex() .w_full() .gap_3() - .child(self.render_row( - crate::i18n::menu_text(cx, "settings.mac.colorTheme").to_string(), - None, - self.render_dropdown( - "general-theme", - s.theme.clone(), - 160.0, - vec![ - ( - "lithe-dark", - crate::i18n::menu_text(cx, "settings.mac.dark") - .to_string(), - ), - ( - "lithe-light", - crate::i18n::menu_text(cx, "settings.mac.light") - .to_string(), - ), - ], - cx, - |this, theme_id, cx| { - // 跟随系统时写入对应的自动主题,否则直接切换主题。 - let sync = settings::get(cx).sync_system_theme; - this.commit_theme(cx, |s| { - if sync { - if crate::theme::ThemePalette::is_light(theme_id) { - s.auto_theme_light = theme_id.to_string(); - } else { - s.auto_theme_dark = theme_id.to_string(); - } - } else { - s.theme = theme_id.to_string(); - } - }) - }, - ), - )) .child( self.render_row( crate::i18n::menu_text(cx, "settings.mac.appearanceMode") - .to_string() .to_string(), Some( crate::i18n::menu_text(