diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bed52955ad..49881e2e7c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -245,6 +245,7 @@ jobs: # 25 was chosen arbitrarily. fetch-depth: 25 clean: false + ref: ${{ github.ref }} - name: Limit target directory size run: script/clear-target-dir-if-larger-than 100 @@ -261,6 +262,9 @@ jobs: mkdir -p target/ # Ignore any errors that occur while drafting release notes to not fail the build. script/draft-release-notes "$RELEASE_VERSION" "$RELEASE_CHANNEL" > target/release-notes.md || true + script/create-draft-release target/release-notes.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Generate license file run: script/generate-licenses @@ -268,18 +272,12 @@ jobs: - name: Create macOS app bundle run: script/bundle-mac - - name: Rename single-architecture binaries + - name: Rename binaries if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }} run: | mv target/aarch64-apple-darwin/release/Zed.dmg target/aarch64-apple-darwin/release/Zed-aarch64.dmg mv target/x86_64-apple-darwin/release/Zed.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg - - name: Upload app bundle (universal) to workflow run if main branch or specific label - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4 - if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }} - with: - name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg - path: target/release/Zed.dmg - name: Upload app bundle (aarch64) to workflow run if main branch or specific label uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4 if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }} @@ -305,8 +303,6 @@ jobs: target/zed-remote-server-macos-aarch64.gz target/aarch64-apple-darwin/release/Zed-aarch64.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg - target/release/Zed.dmg - body_path: target/release-notes.md env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -353,7 +349,6 @@ jobs: files: | target/zed-remote-server-linux-x86_64.gz target/release/zed-linux-x86_64.tar.gz - body: "" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -400,6 +395,18 @@ jobs: files: | target/zed-remote-server-linux-aarch64.gz target/release/zed-linux-aarch64.tar.gz - body: "" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + auto-release-preview: + name: Auto release preview + if: ${{ startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') }} + needs: [bundle-mac, bundle-linux, bundle-linux-aarch64] + runs-on: + - self-hosted + - bundle + steps: + - name: gh release + run: gh release edit $GITHUB_REF_NAME --draft=false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/script_checks.yml b/.github/workflows/script_checks.yml new file mode 100644 index 0000000000..c32a433e46 --- /dev/null +++ b/.github/workflows/script_checks.yml @@ -0,0 +1,21 @@ +name: Script + +on: + pull_request: + paths: + - "script/**" + push: + branches: + - main + +jobs: + shellcheck: + name: "ShellCheck Scripts" + if: github.repository_owner == 'zed-industries' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - name: Shellcheck ./scripts + run: | + ./script/shellcheck-scripts error diff --git a/Cargo.lock b/Cargo.lock index 31e3e83899..05858ebce0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,7 +10,7 @@ dependencies = [ "auto_update", "editor", "extension_host", - "futures 0.3.30", + "futures 0.3.31", "gpui", "language", "lsp", @@ -23,19 +23,13 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ - "gimli 0.31.0", + "gimli 0.31.1", ] -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - [[package]] name = "adler2" version = "2.0.0" @@ -100,8 +94,8 @@ dependencies = [ "miow", "parking_lot", "piper", - "polling 3.7.3", - "regex-automata 0.4.7", + "polling 3.7.4", + "regex-automata 0.4.9", "rustix-openpty", "serde", "signal-hook", @@ -124,9 +118,9 @@ checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" [[package]] name = "alsa" @@ -192,9 +186,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -207,36 +201,36 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -245,13 +239,13 @@ version = "0.1.0" dependencies = [ "anyhow", "chrono", - "futures 0.3.30", + "futures 0.3.31", "http_client", "schemars", "serde", "serde_json", "strum 0.25.0", - "thiserror", + "thiserror 1.0.69", "util", ] @@ -278,9 +272,9 @@ dependencies = [ [[package]] name = "arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" [[package]] name = "arg_enum_proc_macro" @@ -301,9 +295,9 @@ checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" [[package]] name = "arrayref" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" @@ -396,7 +390,7 @@ dependencies = [ "env_logger 0.11.5", "feature_flags", "fs", - "futures 0.3.30", + "futures 0.3.31", "fuzzy", "globset", "gpui", @@ -408,6 +402,8 @@ dependencies = [ "indoc", "language", "language_model", + "language_model_selector", + "language_models", "languages", "log", "lsp", @@ -454,6 +450,24 @@ dependencies = [ "zed_actions", ] +[[package]] +name = "assistant2" +version = "0.1.0" +dependencies = [ + "anyhow", + "command_palette_hooks", + "editor", + "feature_flags", + "gpui", + "language_model", + "language_model_selector", + "proto", + "settings", + "theme", + "ui", + "workspace", +] + [[package]] name = "assistant_slash_command" version = "0.1.0" @@ -463,7 +477,7 @@ dependencies = [ "collections", "derive_more", "extension", - "futures 0.3.30", + "futures 0.3.31", "gpui", "language", "language_model", @@ -573,14 +587,14 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" dependencies = [ "async-task", "concurrent-queue", - "fastrand 2.1.1", - "futures-lite 2.3.0", + "fastrand 2.2.0", + "futures-lite 2.5.0", "slab", ] @@ -604,7 +618,7 @@ checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" dependencies = [ "async-lock 3.4.0", "blocking", - "futures-lite 2.3.0", + "futures-lite 2.5.0", ] [[package]] @@ -615,10 +629,10 @@ checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ "async-channel 2.3.1", "async-executor", - "async-io 2.3.4", + "async-io 2.4.0", "async-lock 3.4.0", "blocking", - "futures-lite 2.3.0", + "futures-lite 2.5.0", "once_cell", ] @@ -644,18 +658,18 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.4" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" dependencies = [ "async-lock 3.4.0", "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.3.0", + "futures-lite 2.5.0", "parking", - "polling 3.7.3", - "rustix 0.38.35", + "polling 3.7.4", + "rustix 0.38.40", "slab", "tracing", "windows-sys 0.59.0", @@ -689,7 +703,7 @@ checksum = "9343dc5acf07e79ff82d0c37899f079db3534d99f189a1837c8e549c99405bec" dependencies = [ "futures-util", "native-tls", - "thiserror", + "thiserror 1.0.69", "url", ] @@ -710,9 +724,9 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" dependencies = [ - "async-io 2.3.4", + "async-io 2.4.0", "blocking", - "futures-lite 2.3.0", + "futures-lite 2.5.0", ] [[package]] @@ -720,7 +734,7 @@ name = "async-pipe" version = "0.1.3" source = "git+https://github.com/zed-industries/async-pipe-rs?rev=82d00a04211cf4e1236029aa03e6b6ce2a74c553#82d00a04211cf4e1236029aa03e6b6ce2a74c553" dependencies = [ - "futures 0.3.30", + "futures 0.3.31", "log", ] @@ -737,28 +751,27 @@ dependencies = [ "cfg-if", "event-listener 3.1.0", "futures-lite 1.13.0", - "rustix 0.38.35", + "rustix 0.38.40", "windows-sys 0.48.0", ] [[package]] name = "async-process" -version = "2.2.4" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a07789659a4d385b79b18b9127fc27e1a59e1e89117c78c5ea3b806f016374" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" dependencies = [ "async-channel 2.3.1", - "async-io 2.3.4", + "async-io 2.4.0", "async-lock 3.4.0", "async-signal", "async-task", "blocking", "cfg-if", "event-listener 5.3.1", - "futures-lite 2.3.0", - "rustix 0.38.35", + "futures-lite 2.5.0", + "rustix 0.38.40", "tracing", - "windows-sys 0.59.0", ] [[package]] @@ -789,13 +802,13 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" dependencies = [ - "async-io 2.3.4", + "async-io 2.4.0", "async-lock 3.4.0", "atomic-waker", "cfg-if", "futures-core", "futures-io", - "rustix 0.38.35", + "rustix 0.38.40", "signal-hook-registry", "slab", "windows-sys 0.59.0", @@ -803,21 +816,21 @@ dependencies = [ [[package]] name = "async-std" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615" dependencies = [ "async-attributes", "async-channel 1.9.0", "async-global-executor", - "async-io 1.13.0", - "async-lock 2.8.0", - "async-process 1.8.1", + "async-io 2.4.0", + "async-lock 3.4.0", + "async-process 2.3.0", "crossbeam-utils", "futures-channel", "futures-core", "futures-io", - "futures-lite 1.13.0", + "futures-lite 2.5.0", "gloo-timers", "kv-log-macro", "log", @@ -831,9 +844,9 @@ dependencies = [ [[package]] name = "async-stream" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ "async-stream-impl", "futures-core", @@ -842,9 +855,9 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", @@ -867,7 +880,7 @@ dependencies = [ "serde_qs 0.10.1", "smart-default", "smol_str", - "thiserror", + "thiserror 1.0.69", "tokio", ] @@ -891,6 +904,20 @@ version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" +[[package]] +name = "async-tls" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfeefd0ca297cbbb3bd34fd6b228401c2a5177038257afd751bc29f0a2da4795" +dependencies = [ + "futures-core", + "futures-io", + "rustls 0.20.9", + "rustls-pemfile 1.0.4", + "webpki", + "webpki-roots 0.22.6", +] + [[package]] name = "async-tls" version = "0.13.0" @@ -915,6 +942,21 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "async-tungstenite" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce01ac37fdc85f10a43c43bc582cbd566720357011578a935761075f898baf58" +dependencies = [ + "async-std", + "async-tls 0.12.0", + "futures-io", + "futures-util", + "log", + "pin-project-lite", + "tungstenite 0.19.0", +] + [[package]] name = "async-tungstenite" version = "0.28.0" @@ -922,7 +964,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e661b6cb0a6eb34d02c520b052daa3aa9ac0cc02495c9d066bbce13ead132b" dependencies = [ "async-std", - "async-tls", + "async-tls 0.13.0", "futures-io", "futures-util", "log", @@ -947,9 +989,9 @@ checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52" dependencies = [ "async-compression", "crc32fast", - "futures-lite 2.3.0", + "futures-lite 2.5.0", "pin-project", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -958,7 +1000,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "futures-sink", "futures-util", "memchr", @@ -1006,31 +1048,46 @@ dependencies = [ "anyhow", "client", "db", - "editor", "gpui", "http_client", "log", - "markdown_preview", - "menu", "paths", "release_channel", "schemars", "serde", - "serde_derive", "serde_json", "settings", "smol", "tempfile", - "util", "which 6.0.3", "workspace", ] +[[package]] +name = "auto_update_ui" +version = "0.1.0" +dependencies = [ + "anyhow", + "auto_update", + "client", + "editor", + "gpui", + "http_client", + "markdown_preview", + "menu", + "release_channel", + "serde", + "serde_json", + "smol", + "util", + "workspace", +] + [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "av1-grain" @@ -1048,18 +1105,18 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2" +checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62" dependencies = [ "arrayvec", ] [[package]] name = "aws-config" -version = "1.5.5" +version = "1.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e95816a168520d72c0e7680c405a5a8c1fb6a035b4bc4b9d7b0de8e1a941697" +checksum = "9b49afaa341e8dd8577e1a2200468f98956d6eda50bcf4a53246cc00174ba924" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1073,11 +1130,11 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", - "bytes 1.7.2", - "fastrand 2.1.1", + "bytes 1.8.0", + "fastrand 2.2.0", "hex", "http 0.2.12", - "ring", + "ring 0.17.8", "time", "tokio", "tracing", @@ -1112,8 +1169,8 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", - "bytes 1.7.2", - "fastrand 2.1.1", + "bytes 1.8.0", + "fastrand 2.2.0", "http 0.2.12", "http-body 0.4.6", "once_cell", @@ -1138,7 +1195,7 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", - "bytes 1.7.2", + "bytes 1.8.0", "http 0.2.12", "once_cell", "regex-lite", @@ -1147,11 +1204,10 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.47.0" +version = "1.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cca49303c05d2a740b8a4552fac63a4db6ead84f7e7eeed04761fd3014c26f25" +checksum = "0e531658a0397d22365dfe26c3e1c0c8448bf6a3a2d8a098ded802f2b1261615" dependencies = [ - "ahash 0.8.11", "aws-credential-types", "aws-runtime", "aws-sigv4", @@ -1165,8 +1221,8 @@ dependencies = [ "aws-smithy-types", "aws-smithy-xml", "aws-types", - "bytes 1.7.2", - "fastrand 2.1.1", + "bytes 1.8.0", + "fastrand 2.2.0", "hex", "hmac", "http 0.2.12", @@ -1182,9 +1238,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.40.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5879bec6e74b648ce12f6085e7245417bc5f6d672781028384d2e494be3eb6d" +checksum = "09677244a9da92172c8dc60109b4a9658597d4d298b188dd0018b6a66b410ca4" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1195,7 +1251,7 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", - "bytes 1.7.2", + "bytes 1.8.0", "http 0.2.12", "once_cell", "regex-lite", @@ -1204,9 +1260,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.41.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ef4cd9362f638c22a3b959fd8df292e7e47fdf170270f86246b97109b5f2f7d" +checksum = "81fea2f3a8bb3bd10932ae7ad59cc59f65f270fc9183a7e91f501dc5efbef7ee" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1217,7 +1273,7 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", - "bytes 1.7.2", + "bytes 1.8.0", "http 0.2.12", "once_cell", "regex-lite", @@ -1226,9 +1282,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.40.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b1e2735d2ab28b35ecbb5496c9d41857f52a0d6a0075bbf6a8af306045ea6f6" +checksum = "6ada54e5f26ac246dc79727def52f7f8ed38915cb47781e2a72213957dc3a7d5" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1258,7 +1314,7 @@ dependencies = [ "aws-smithy-http", "aws-smithy-runtime-api", "aws-smithy-types", - "bytes 1.7.2", + "bytes 1.8.0", "crypto-bigint 0.5.5", "form_urlencoded", "hex", @@ -1268,7 +1324,7 @@ dependencies = [ "once_cell", "p256", "percent-encoding", - "ring", + "ring 0.17.8", "sha2", "subtle", "time", @@ -1289,13 +1345,13 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.60.12" +version = "0.60.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598b1689d001c4d4dc3cb386adb07d37786783aee3ac4b324bcadac116bf3d23" +checksum = "ba1a71073fca26775c8b5189175ea8863afb1c9ea2cceb02a5de5ad9dfbaa795" dependencies = [ "aws-smithy-http", "aws-smithy-types", - "bytes 1.7.2", + "bytes 1.8.0", "crc32c", "crc32fast", "hex", @@ -1315,7 +1371,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cef7d0a272725f87e51ba2bf89f8c21e4df61b9e49ae1ac367a6d69916ef7c90" dependencies = [ "aws-smithy-types", - "bytes 1.7.2", + "bytes 1.8.0", "crc32fast", ] @@ -1328,7 +1384,7 @@ dependencies = [ "aws-smithy-eventstream", "aws-smithy-runtime-api", "aws-smithy-types", - "bytes 1.7.2", + "bytes 1.8.0", "bytes-utils", "futures-core", "http 0.2.12", @@ -1369,8 +1425,8 @@ dependencies = [ "aws-smithy-http", "aws-smithy-runtime-api", "aws-smithy-types", - "bytes 1.7.2", - "fastrand 2.1.1", + "bytes 1.8.0", + "fastrand 2.2.0", "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", @@ -1394,7 +1450,7 @@ checksum = "92165296a47a812b267b4f41032ff8069ab7ff783696d217f0994a0d7ab585cd" dependencies = [ "aws-smithy-async", "aws-smithy-types", - "bytes 1.7.2", + "bytes 1.8.0", "http 0.2.12", "http 1.1.0", "pin-project-lite", @@ -1410,7 +1466,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fbd94a32b3a7d55d3806fe27d98d3ad393050439dd05eb53ece36ec5e3d3510" dependencies = [ "base64-simd", - "bytes 1.7.2", + "bytes 1.8.0", "bytes-utils", "futures-core", "http 0.2.12", @@ -1431,9 +1487,9 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.8" +version = "0.60.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d123fbc2a4adc3c301652ba8e149bf4bc1d1725affb9784eb20c953ace06bf55" +checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" dependencies = [ "xmlparser", ] @@ -1462,7 +1518,7 @@ dependencies = [ "axum-core", "base64 0.21.7", "bitflags 1.3.2", - "bytes 1.7.2", + "bytes 1.8.0", "futures-util", "headers", "http 0.2.12", @@ -1495,7 +1551,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" dependencies = [ "async-trait", - "bytes 1.7.2", + "bytes 1.8.0", "futures-util", "http 0.2.12", "http-body 0.4.6", @@ -1512,7 +1568,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9a320103719de37b7b4da4c8eb629d4573f6bcfd3dfe80d3208806895ccf81d" dependencies = [ "axum", - "bytes 1.7.2", + "bytes 1.8.0", "futures-util", "http 0.2.12", "mime", @@ -1535,7 +1591,7 @@ dependencies = [ "addr2line", "cfg-if", "libc", - "miniz_oxide 0.8.0", + "miniz_oxide", "object", "rustc-demangle", "windows-targets 0.52.6", @@ -1583,9 +1639,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bigdecimal" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d712318a27c7150326677b321a5fa91b55f6d9034ffd67f20319e147d40cee" +checksum = "8f850665a0385e070b64c38d2354e6c104c8479c59868d1e48a0c13ee2c7a1c1" dependencies = [ "autocfg", "libm", @@ -1604,26 +1660,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bindgen" -version = "0.69.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" -dependencies = [ - "bitflags 2.6.0", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.87", -] - [[package]] name = "bindgen" version = "0.70.1" @@ -1712,9 +1748,9 @@ dependencies = [ [[package]] name = "bitstream-io" -version = "2.5.3" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" [[package]] name = "bitvec" @@ -1825,15 +1861,15 @@ dependencies = [ "async-channel 2.3.1", "async-task", "futures-io", - "futures-lite 2.3.0", + "futures-lite 2.5.0", "piper", ] [[package]] name = "borsh" -version = "1.5.1" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" +checksum = "2506947f73ad44e344215ccd6403ac2ae18cd8e046e581a441bf8d199f257f03" dependencies = [ "borsh-derive", "cfg_aliases 0.2.1", @@ -1841,16 +1877,15 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.5.1" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" +checksum = "c2593a3b8b938bd68373196c9832f516be11fa487ef4ae745eb282e6a56a7244" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.87", - "syn_derive", ] [[package]] @@ -1868,20 +1903,20 @@ dependencies = [ [[package]] name = "bstr" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" dependencies = [ "memchr", - "regex-automata 0.4.7", + "regex-automata 0.4.9", "serde", ] [[package]] name = "built" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "236e6289eda5a812bc6b53c3b024039382a2895fbbeef2d748b2931546d392c4" +checksum = "c360505aed52b7ec96a3636c3f039d99103c37d1d9b4f7a8c743d3ea9ffcd03b" [[package]] name = "bumpalo" @@ -1919,18 +1954,18 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.17.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773d90827bc3feecfb67fab12e24de0749aad83c74b9504ecde46237b5cd24e2" +checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc8b54b395f2fcfbb3d90c47b01c7f444d94d05bdeb775811dec868ac3bbc26" +checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec" dependencies = [ "proc-macro2", "quote", @@ -1961,9 +1996,9 @@ dependencies = [ [[package]] name = "bytes" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "bytes-utils" @@ -1971,7 +2006,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "either", ] @@ -1984,7 +2019,7 @@ dependencies = [ "client", "collections", "fs", - "futures 0.3.30", + "futures 0.3.31", "gpui", "http_client", "language", @@ -2007,10 +2042,10 @@ checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ "bitflags 2.6.0", "log", - "polling 3.7.3", - "rustix 0.38.35", + "polling 3.7.4", + "rustix 0.38.40", "slab", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2020,7 +2055,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" dependencies = [ "calloop", - "rustix 0.38.35", + "rustix 0.38.40", "wayland-backend", "wayland-client", ] @@ -2036,9 +2071,9 @@ dependencies = [ [[package]] name = "cap-fs-ext" -version = "3.2.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb23061fc1c4ead4e45ca713080fe768e6234e959f5a5c399c39eb41aa34e56e" +checksum = "e16619ada836f12897a72011fe99b03f0025b87a8dbbea4f3c9f89b458a23bf3" dependencies = [ "cap-primitives", "cap-std", @@ -2048,21 +2083,21 @@ dependencies = [ [[package]] name = "cap-net-ext" -version = "3.2.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83ae11f116bcbafc5327c6af250341db96b5930046732e1905f7dc65887e0e1" +checksum = "710b0eb776410a22c89a98f2f80b2187c2ac3a8206b99f3412332e63c9b09de0" dependencies = [ "cap-primitives", "cap-std", - "rustix 0.38.35", + "rustix 0.38.40", "smallvec", ] [[package]] name = "cap-primitives" -version = "3.2.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d00bd8d26c4270d950eaaa837387964a2089a1c3c349a690a1fa03221d29531" +checksum = "82fa6c3f9773feab88d844aa50035a33fb6e7e7426105d2f4bb7aadc42a5f89a" dependencies = [ "ambient-authority", "fs-set-times", @@ -2070,16 +2105,16 @@ dependencies = [ "io-lifetimes 2.0.3", "ipnet", "maybe-owned", - "rustix 0.38.35", + "rustix 0.38.40", "windows-sys 0.52.0", "winx", ] [[package]] name = "cap-rand" -version = "3.2.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbcb16a619d8b8211ed61f42bd290d2a1ac71277a69cf8417ec0996fa92f5211" +checksum = "53774d49369892b70184f8312e50c1b87edccb376691de4485b0ff554b27c36c" dependencies = [ "ambient-authority", "rand 0.8.5", @@ -2087,27 +2122,27 @@ dependencies = [ [[package]] name = "cap-std" -version = "3.2.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19eb8e3d71996828751c1ed3908a439639752ac6bdc874e41469ef7fc15fbd7f" +checksum = "7f71b70818556b4fe2a10c7c30baac3f5f45e973f49fc2673d7c75c39d0baf5b" dependencies = [ "cap-primitives", "io-extras", "io-lifetimes 2.0.3", - "rustix 0.38.35", + "rustix 0.38.40", ] [[package]] name = "cap-time-ext" -version = "3.2.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61142dc51e25b7acc970ca578ce2c3695eac22bbba46c1073f5f583e78957725" +checksum = "69dd48afa2363f746c93f961c211f6f099fb594a3446b8097bc5f79db51b6816" dependencies = [ "ambient-authority", "cap-primitives", "iana-time-zone", "once_cell", - "rustix 0.38.35", + "rustix 0.38.40", "winx", ] @@ -2122,16 +2157,16 @@ dependencies = [ [[package]] name = "cargo_metadata" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +checksum = "afc309ed89476c8957c50fb818f56fe894db857866c3e163335faa91dc34eb85" dependencies = [ "camino", "cargo-platform", "semver", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2166,7 +2201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fce8dd7fcfcbf3a0a87d8f515194b49d6135acab73e18bd380d1d93bb1a15eb" dependencies = [ "heck 0.4.1", - "indexmap 2.4.0", + "indexmap 2.6.0", "log", "proc-macro2", "quote", @@ -2179,9 +2214,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.15" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" dependencies = [ "jobserver", "libc", @@ -2239,7 +2274,7 @@ dependencies = [ "client", "clock", "collections", - "futures 0.3.30", + "futures 0.3.31", "gpui", "http_client", "language", @@ -2349,9 +2384,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.24" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7db6eca8c205649e8d3ccd05aa5042b1800a784e56bc7c43524fde8abbfa9b" +checksum = "d9647a559c112175f17cf724dc72d3645680a883c58481332779192b0d8e7a01" dependencies = [ "clap", ] @@ -2370,9 +2405,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" [[package]] name = "cli" @@ -2403,17 +2438,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0875e527e299fc5f4faba42870bf199a39ab0bb2dbba1b8aef0a2151451130f" dependencies = [ "bstr", - "bytes 1.7.2", + "bytes 1.8.0", "clickhouse-derive", "clickhouse-rs-cityhash-sys", - "futures 0.3.30", + "futures 0.3.31", "hyper 0.14.31", "hyper-tls", "lz4", "sealed", "serde", "static_assertions", - "thiserror", + "thiserror 1.0.69", "tokio", "url", ] @@ -2446,13 +2481,13 @@ dependencies = [ "anyhow", "async-native-tls", "async-recursion 0.3.2", - "async-tungstenite", + "async-tungstenite 0.28.0", "chrono", "clock", "cocoa 0.26.0", "collections", "feature_flags", - "futures 0.3.30", + "futures 0.3.31", "gpui", "http_client", "log", @@ -2471,10 +2506,9 @@ dependencies = [ "settings", "sha2", "smol", - "sysinfo", "telemetry_events", "text", - "thiserror", + "thiserror 1.0.69", "time", "tiny_http", "tokio-socks", @@ -2488,7 +2522,6 @@ dependencies = [ name = "clock" version = "0.1.0" dependencies = [ - "chrono", "parking_lot", "serde", "smallvec", @@ -2579,7 +2612,7 @@ dependencies = [ "assistant", "async-stripe", "async-trait", - "async-tungstenite", + "async-tungstenite 0.28.0", "audio", "aws-config", "aws-sdk-kinesis", @@ -2597,14 +2630,15 @@ dependencies = [ "collections", "context_servers", "ctor", - "dashmap 6.0.1", + "dashmap 6.1.0", "derive_more", "editor", "env_logger 0.11.5", "envy", + "extension", "file_finder", "fs", - "futures 0.3.30", + "futures 0.3.31", "git", "git_hosting_providers", "google_ai", @@ -2657,7 +2691,7 @@ dependencies = [ "telemetry_events", "text", "theme", - "thiserror", + "thiserror 1.0.69", "time", "tokio", "toml 0.8.19", @@ -2685,7 +2719,7 @@ dependencies = [ "db", "editor", "emojis", - "futures 0.3.30", + "futures 0.3.31", "fuzzy", "gpui", "http_client", @@ -2713,7 +2747,6 @@ dependencies = [ "tree-sitter-md", "ui", "util", - "vcs_menu", "workspace", ] @@ -2732,9 +2765,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "combine" @@ -2742,7 +2775,7 @@ version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "memchr", ] @@ -2844,7 +2877,8 @@ dependencies = [ "anyhow", "collections", "command_palette_hooks", - "futures 0.3.30", + "extension", + "futures 0.3.31", "gpui", "log", "parking_lot", @@ -2889,10 +2923,11 @@ dependencies = [ "command_palette_hooks", "editor", "fs", - "futures 0.3.30", + "futures 0.3.31", "gpui", "http_client", "indoc", + "inline_completion", "language", "lsp", "menu", @@ -3022,11 +3057,11 @@ dependencies = [ [[package]] name = "coreaudio-sys" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f01585027057ff5f0a5bf276174ae4c1594a2c5bde93d5f46a016d76270f5a9" +checksum = "2ce857aa0b77d77287acc1ac3e37a05a8c95a2af3647d23b15f263bdaeb7562b" dependencies = [ - "bindgen 0.69.4", + "bindgen", ] [[package]] @@ -3085,9 +3120,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.13" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6" dependencies = [ "libc", ] @@ -3367,9 +3402,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", "syn 2.0.87", @@ -3400,7 +3435,7 @@ dependencies = [ "collections", "dap-types", "fs", - "futures 0.3.30", + "futures 0.3.31", "gpui", "http_client", "log", @@ -3459,9 +3494,9 @@ dependencies = [ [[package]] name = "dashmap" -version = "6.0.1" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ "cfg-if", "crossbeam-utils", @@ -3524,7 +3559,7 @@ dependencies = [ "anyhow", "dap", "editor", - "futures 0.3.30", + "futures 0.3.31", "gpui", "project", "smol", @@ -3540,7 +3575,7 @@ dependencies = [ "command_palette_hooks", "dap", "editor", - "futures 0.3.30", + "futures 0.3.31", "fuzzy", "gpui", "language", @@ -3657,7 +3692,7 @@ dependencies = [ "fuzzy-matcher", "shell-words", "tempfile", - "thiserror", + "thiserror 1.0.69", "zeroize", ] @@ -3720,6 +3755,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "dlib" version = "0.5.2" @@ -3766,9 +3812,9 @@ dependencies = [ [[package]] name = "dwrote" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da3498378ed373237bdef1eddcc64e7be2d3ba4841f4c22a998e81cadeea83c" +checksum = "70182709525a3632b2ba96b6569225467b18ecb4a77f46d255f713a6bebf05fd" dependencies = [ "lazy_static", "libc", @@ -3817,12 +3863,14 @@ dependencies = [ "emojis", "env_logger 0.11.5", "file_icons", - "futures 0.3.30", + "fs", + "futures 0.3.31", "fuzzy", "git", "gpui", "http_client", "indoc", + "inline_completion", "itertools 0.13.0", "language", "linkify", @@ -3864,18 +3912,6 @@ dependencies = [ "workspace", ] -[[package]] -name = "educe" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4bd92664bf78c4d3dba9b7cdafce6fa15b13ed3ed16175218196942e99168a8" -dependencies = [ - "enum-ordinalize", - "proc-macro2", - "quote", - "syn 2.0.87", -] - [[package]] name = "either" version = "1.13.0" @@ -3919,9 +3955,9 @@ dependencies = [ [[package]] name = "embed-resource" -version = "2.4.3" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4edcacde9351c33139a41e3c97eb2334351a81a2791bebb0b243df837128f602" +checksum = "b68b6f9f63a0b6a38bc447d4ce84e2b388f3ec95c99c641c8ff0dd3ef89a6379" dependencies = [ "cc", "memchr", @@ -3960,9 +3996,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -3973,26 +4009,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" -[[package]] -name = "enum-ordinalize" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" -dependencies = [ - "enum-ordinalize-derive", -] - -[[package]] -name = "enum-ordinalize-derive" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.87", -] - [[package]] name = "enumflags2" version = "0.7.10" @@ -4162,6 +4178,7 @@ dependencies = [ "serde_json", "settings", "smol", + "util", ] [[package]] @@ -4214,15 +4231,14 @@ dependencies = [ [[package]] name = "exr" -version = "1.72.0" +version = "1.73.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" dependencies = [ "bit_field", - "flume", "half", "lebe", - "miniz_oxide 0.7.4", + "miniz_oxide", "rayon-core", "smallvec", "zune-inflate", @@ -4238,16 +4254,18 @@ dependencies = [ "async-trait", "collections", "fs", - "futures 0.3.30", + "futures 0.3.31", "gpui", "http_client", "language", "log", "lsp", + "parking_lot", "semantic_version", "serde", "serde_json", "toml 0.8.19", + "util", "wasm-encoder 0.215.0", "wasmparser 0.215.0", "wit-component", @@ -4290,10 +4308,11 @@ dependencies = [ "env_logger 0.11.5", "extension", "fs", - "futures 0.3.30", + "futures 0.3.31", "gpui", "http_client", "language", + "language_extension", "log", "lsp", "node_runtime", @@ -4301,6 +4320,7 @@ dependencies = [ "paths", "project", "release_channel", + "remote", "reqwest_client", "schemars", "semantic_version", @@ -4309,7 +4329,9 @@ dependencies = [ "serde_json_lenient", "settings", "task", + "tempfile", "theme", + "theme_extension", "toml 0.8.19", "url", "util", @@ -4323,21 +4345,15 @@ name = "extensions_ui" version = "0.1.0" dependencies = [ "anyhow", - "assistant_slash_command", "client", "collections", - "context_servers", "db", "editor", - "extension", "extension_host", "fs", "fuzzy", "gpui", - "indexed_docs", "language", - "log", - "lsp", "num-format", "picker", "project", @@ -4346,14 +4362,12 @@ dependencies = [ "serde", "settings", "smallvec", - "snippet_provider", "theme", - "theme_selector", "ui", "util", - "vim", - "wasmtime-wasi", + "vim_mode_setting", "workspace", + "zed_actions", ] [[package]] @@ -4379,8 +4393,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" dependencies = [ "bit-set 0.8.0", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", ] [[package]] @@ -4400,9 +4414,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" [[package]] name = "fd-lock" @@ -4411,15 +4425,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" dependencies = [ "cfg-if", - "rustix 0.38.35", + "rustix 0.38.40", "windows-sys 0.52.0", ] [[package]] name = "fdeflate" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +checksum = "07c6f4c64c1d33a3111c4466f7365ebdcc37c5bd1ea0d62aae2e3d722aacbedb" dependencies = [ "simd-adler32", ] @@ -4428,7 +4442,7 @@ dependencies = [ name = "feature_flags" version = "0.1.0" dependencies = [ - "futures 0.3.30", + "futures 0.3.31", "gpui", ] @@ -4441,7 +4455,7 @@ dependencies = [ "client", "db", "editor", - "futures 0.3.30", + "futures 0.3.31", "gpui", "http_client", "human_bytes", @@ -4460,6 +4474,7 @@ dependencies = [ "urlencoding", "util", "workspace", + "zed_actions", ] [[package]] @@ -4482,7 +4497,7 @@ dependencies = [ "editor", "env_logger 0.11.5", "file_icons", - "futures 0.3.30", + "futures 0.3.31", "fuzzy", "gpui", "language", @@ -4520,7 +4535,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e" dependencies = [ "libc", - "thiserror", + "thiserror 1.0.69", "winapi", ] @@ -4544,12 +4559,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", - "miniz_oxide 0.8.0", + "miniz_oxide", ] [[package]] @@ -4573,7 +4588,7 @@ dependencies = [ "futures-core", "futures-sink", "nanorand", - "spin", + "spin 0.9.8", ] [[package]] @@ -4582,6 +4597,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + [[package]] name = "font-kit" version = "0.14.1" @@ -4608,9 +4629,9 @@ dependencies = [ [[package]] name = "font-types" -version = "0.6.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f0189ccb084f77c5523e08288d418cbaa09c451a08515678a0aa265df9a8b60" +checksum = "b3971f9a5ca983419cdc386941ba3b9e1feba01a0ab888adf78739feb2798492" dependencies = [ "bytemuck", ] @@ -4720,7 +4741,7 @@ dependencies = [ "cocoa 0.26.0", "collections", "fsevent", - "futures 0.3.30", + "futures 0.3.31", "git", "git2", "gpui", @@ -4729,6 +4750,7 @@ dependencies = [ "objc", "parking_lot", "paths", + "proto", "rope", "serde", "serde_json", @@ -4747,7 +4769,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "033b337d725b97690d86893f9de22b67b80dcc4e9ad815f348254c38119db8fb" dependencies = [ "io-lifetimes 2.0.3", - "rustix 0.38.35", + "rustix 0.38.40", "windows-sys 0.52.0", ] @@ -4804,9 +4826,9 @@ checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -4823,16 +4845,16 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f444c45a1cb86f2a7e301469fd50a82084a60dadc25d94529a8312276ecb71a" dependencies = [ - "futures 0.3.30", + "futures 0.3.31", "futures-timer", "pin-utils", ] [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -4840,15 +4862,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -4868,9 +4890,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" @@ -4889,11 +4911,11 @@ dependencies = [ [[package]] name = "futures-lite" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" dependencies = [ - "fastrand 2.1.1", + "fastrand 2.2.0", "futures-core", "futures-io", "parking", @@ -4902,9 +4924,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -4913,15 +4935,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-timer" @@ -4931,9 +4953,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures 0.1.31", "futures-channel", @@ -5027,15 +5049,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" dependencies = [ "fallible-iterator", - "indexmap 2.4.0", + "indexmap 2.6.0", "stable_deref_trait", ] [[package]] name = "gimli" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "git" @@ -5062,7 +5084,6 @@ dependencies = [ "unindent", "url", "util", - "windows 0.58.0", ] [[package]] @@ -5084,7 +5105,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "futures 0.3.30", + "futures 0.3.31", "git", "gpui", "http_client", @@ -5112,15 +5133,15 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", ] [[package]] name = "gloo-timers" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" dependencies = [ "futures-channel", "futures-core", @@ -5130,9 +5151,9 @@ dependencies = [ [[package]] name = "glow" -version = "0.14.0" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f865cbd94bd355b89611211e49508da98a1fce0ad755c1e8448fb96711b24528" +checksum = "d51fa363f025f5c111e03f13eda21162faeacb6911fe8caa0c0349f9cf0c4483" dependencies = [ "js-sys", "slotmap", @@ -5170,7 +5191,7 @@ name = "google_ai" version = "0.1.0" dependencies = [ "anyhow", - "futures 0.3.30", + "futures 0.3.31", "http_client", "schemars", "serde", @@ -5217,7 +5238,7 @@ dependencies = [ "ashpd", "async-task", "backtrace", - "bindgen 0.70.1", + "bindgen", "blade-graphics", "blade-macros", "blade-util", @@ -5242,7 +5263,7 @@ dependencies = [ "flume", "font-kit", "foreign-types 0.5.0", - "futures 0.3.30", + "futures 0.3.31", "gpui_macros", "http_client", "image", @@ -5276,7 +5297,7 @@ dependencies = [ "strum 0.25.0", "sum_tree", "taffy", - "thiserror", + "thiserror 1.0.69", "unicode-segmentation", "usvg", "util", @@ -5327,13 +5348,13 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "fnv", "futures-core", "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.4.0", + "indexmap 2.6.0", "slab", "tokio", "tokio-util", @@ -5347,12 +5368,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" dependencies = [ "atomic-waker", - "bytes 1.7.2", + "bytes 1.8.0", "fnv", "futures-core", "futures-sink", "http 1.1.0", - "indexmap 2.4.0", + "indexmap 2.6.0", "slab", "tokio", "tokio-util", @@ -5380,7 +5401,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -5395,7 +5416,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -5427,6 +5448,17 @@ dependencies = [ "serde", ] +[[package]] +name = "hashbrown" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashlink" version = "0.8.4" @@ -5452,7 +5484,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ "base64 0.21.7", - "bytes 1.7.2", + "bytes 1.8.0", "headers-core", "http 0.2.12", "httpdate", @@ -5631,7 +5663,7 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "fnv", "itoa", ] @@ -5642,7 +5674,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "fnv", "itoa", ] @@ -5653,7 +5685,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "http 0.2.12", "pin-project-lite", ] @@ -5664,7 +5696,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "http 1.1.0", ] @@ -5674,7 +5706,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "futures-util", "http 1.1.0", "http-body 1.0.1", @@ -5713,9 +5745,9 @@ name = "http_client" version = "0.1.0" dependencies = [ "anyhow", - "bytes 1.7.2", + "bytes 1.8.0", "derive_more", - "futures 0.3.30", + "futures 0.3.31", "http 1.1.0", "log", "serde", @@ -5725,9 +5757,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" @@ -5753,7 +5785,7 @@ version = "0.14.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "futures-channel", "futures-core", "futures-util", @@ -5773,11 +5805,11 @@ dependencies = [ [[package]] name = "hyper" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "futures-channel", "futures-util", "h2 0.4.6", @@ -5815,9 +5847,9 @@ checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-util", - "rustls 0.23.13", + "rustls 0.23.16", "rustls-native-certs 0.8.0", "rustls-pki-types", "tokio", @@ -5831,7 +5863,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "hyper 0.14.31", "native-tls", "tokio", @@ -5840,16 +5872,16 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "futures-channel", "futures-util", "http 1.1.0", "http-body 1.0.1", - "hyper 1.4.1", + "hyper 1.5.0", "pin-project-lite", "socket2 0.5.7", "tokio", @@ -5859,9 +5891,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -5880,6 +5912,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "id-arena" version = "2.2.1" @@ -5888,12 +6038,23 @@ checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -5906,7 +6067,7 @@ dependencies = [ "globset", "log", "memchr", - "regex-automata 0.4.7", + "regex-automata 0.4.9", "same-file", "walkdir", "winapi-util", @@ -5914,9 +6075,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.2" +version = "0.25.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" dependencies = [ "bytemuck", "byteorder-lite", @@ -5937,9 +6098,9 @@ dependencies = [ [[package]] name = "image-webp" -version = "0.1.3" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" +checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f" dependencies = [ "byteorder-lite", "quick-error", @@ -5969,9 +6130,9 @@ checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" [[package]] name = "imgref" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" [[package]] name = "indexed_docs" @@ -5984,7 +6145,7 @@ dependencies = [ "derive_more", "extension", "fs", - "futures 0.3.30", + "futures 0.3.31", "fuzzy", "gpui", "heed", @@ -6013,12 +6174,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.15.1", "serde", ] @@ -6045,6 +6206,16 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "inline_completion" +version = "0.1.0" +dependencies = [ + "gpui", + "language", + "project", + "text", +] + [[package]] name = "inline_completion_button" version = "0.1.0" @@ -6053,7 +6224,7 @@ dependencies = [ "copilot", "editor", "fs", - "futures 0.3.30", + "futures 0.3.31", "gpui", "indoc", "language", @@ -6065,7 +6236,6 @@ dependencies = [ "supermaven", "theme", "ui", - "util", "workspace", "zed_actions", ] @@ -6132,9 +6302,9 @@ dependencies = [ [[package]] name = "io-extras" -version = "0.18.2" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9f046b9af244f13b3bd939f55d16830ac3a201e8a9ba9661bfcb03e2be72b9b" +checksum = "7d45fd7584f9b67ac37bc041212d06bfac0700b36456b05890d36a3b626260eb" dependencies = [ "io-lifetimes 2.0.3", "windows-sys 0.52.0", @@ -6187,9 +6357,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" [[package]] name = "is-docker" @@ -6271,7 +6441,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] @@ -6315,9 +6485,9 @@ checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] @@ -6331,22 +6501,58 @@ dependencies = [ "base64 0.21.7", "js-sys", "pem", - "ring", + "ring 0.17.8", "serde", "serde_json", "simple_asn1", ] [[package]] -name = "jupyter-serde" -version = "0.4.0" +name = "jupyter-protocol" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd71aa17c4fa65e6d7536ab2728881a41f8feb2ee5841c2240516c3c3d65d8b3" +checksum = "3d4d496ac890e14efc12c5289818b3c39e3026a7bb02d5576b011e1a062d4bcc" +dependencies = [ + "anyhow", + "async-trait", + "bytes 1.8.0", + "chrono", + "futures 0.3.31", + "jupyter-serde", + "rand 0.8.5", + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "jupyter-serde" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32aa595c3912167b7eafcaa822b767ad1fa9605a18127fc9ac741241b796410e" dependencies = [ "anyhow", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "jupyter-websocket-client" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5850894210a3f033ff730d6f956b0335db38573ce7bb61c6abbf69dcbe284ba7" +dependencies = [ + "anyhow", + "async-trait", + "async-tungstenite 0.22.2", + "futures 0.3.31", + "jupyter-protocol", + "jupyter-serde", + "serde", + "serde_json", + "url", "uuid", ] @@ -6382,9 +6588,9 @@ dependencies = [ [[package]] name = "kurbo" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e5aa9f0f96a938266bdb12928a67169e8d22c6a786fda8ed984b85e6ba93c3c" +checksum = "89234b2cc610a7dd927ebde6b41dd1a5d4214cffaef4cf1fb2195d592f92518f" dependencies = [ "arrayvec", "smallvec", @@ -6411,7 +6617,8 @@ dependencies = [ "ctor", "ec4rs", "env_logger 0.11.5", - "futures 0.3.30", + "fs", + "futures 0.3.31", "fuzzy", "git", "globset", @@ -6455,6 +6662,23 @@ dependencies = [ "util", ] +[[package]] +name = "language_extension" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "collections", + "extension", + "futures 0.3.31", + "gpui", + "language", + "lsp", + "serde", + "serde_json", + "util", +] + [[package]] name = "language_model" version = "0.1.0" @@ -6462,28 +6686,62 @@ dependencies = [ "anthropic", "anyhow", "base64 0.22.1", - "client", "collections", - "copilot", - "ctor", - "editor", - "env_logger 0.11.5", - "feature_flags", - "futures 0.3.30", + "futures 0.3.31", "google_ai", "gpui", "http_client", "image", - "inline_completion_button", - "language", "log", - "menu", "ollama", "open_ai", "parking_lot", + "proto", + "schemars", + "serde", + "serde_json", + "smol", + "strum 0.25.0", + "ui", + "util", +] + +[[package]] +name = "language_model_selector" +version = "0.1.0" +dependencies = [ + "feature_flags", + "gpui", + "language_model", + "picker", + "proto", + "ui", + "workspace", + "zed_actions", +] + +[[package]] +name = "language_models" +version = "0.1.0" +dependencies = [ + "anthropic", + "anyhow", + "client", + "collections", + "copilot", + "editor", + "feature_flags", + "fs", + "futures 0.3.31", + "google_ai", + "gpui", + "http_client", + "language_model", + "menu", + "ollama", + "open_ai", "project", "proto", - "rand 0.8.5", "schemars", "serde", "serde_json", @@ -6491,12 +6749,10 @@ dependencies = [ "smol", "strum 0.25.0", "telemetry_events", - "text", "theme", - "thiserror", + "thiserror 1.0.69", "tiktoken-rs", "ui", - "unindent", "util", ] @@ -6526,7 +6782,7 @@ dependencies = [ "copilot", "editor", "env_logger 0.11.5", - "futures 0.3.30", + "futures 0.3.31", "gpui", "itertools 0.13.0", "language", @@ -6552,7 +6808,7 @@ dependencies = [ "async-tar", "async-trait", "collections", - "futures 0.3.30", + "futures 0.3.31", "gpui", "http_client", "language", @@ -6606,15 +6862,9 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin", + "spin 0.9.8", ] -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "leb128" version = "0.2.5" @@ -6629,9 +6879,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.162" +version = "0.2.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" +checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" [[package]] name = "libdbus-sys" @@ -6645,13 +6895,12 @@ dependencies = [ [[package]] name = "libfuzzer-sys" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +checksum = "9b9569d2f74e257076d8c6bfa73fb505b46b851e51ddaecc825944aa3bed17fa" dependencies = [ "arbitrary", "cc", - "once_cell", ] [[package]] @@ -6678,9 +6927,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libmimalloc-sys" @@ -6700,7 +6949,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.6.0", "libc", - "redox_syscall 0.5.3", + "redox_syscall 0.5.7", ] [[package]] @@ -6767,6 +7016,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + [[package]] name = "live_kit_client" version = "0.1.0" @@ -6776,7 +7031,7 @@ dependencies = [ "async-trait", "collections", "core-foundation 0.9.4", - "futures 0.3.30", + "futures 0.3.31", "gpui", "live_kit_server", "log", @@ -6847,11 +7102,11 @@ dependencies = [ [[package]] name = "lru" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.1", ] [[package]] @@ -6863,7 +7118,7 @@ dependencies = [ "collections", "ctor", "env_logger 0.11.5", - "futures 0.3.30", + "futures 0.3.31", "gpui", "log", "lsp-types", @@ -6875,7 +7130,6 @@ dependencies = [ "serde_json", "smol", "util", - "windows 0.58.0", ] [[package]] @@ -6892,19 +7146,18 @@ dependencies = [ [[package]] name = "lz4" -version = "1.26.0" +version = "1.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958b4caa893816eea05507c20cfe47574a43d9a697138a7872990bba8a0ece68" +checksum = "4d1febb2b4a79ddd1980eede06a8f7902197960aa0383ffcfdd62fe723036725" dependencies = [ - "libc", "lz4-sys", ] [[package]] name = "lz4-sys" -version = "1.10.0" +version = "1.11.1+lz4-1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109de74d5d2353660401699a4174a4ff23fcc649caf553df71933c7fb45ad868" +checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" dependencies = [ "cc", "libc", @@ -6947,7 +7200,7 @@ dependencies = [ "anyhow", "assets", "env_logger 0.11.5", - "futures 0.3.30", + "futures 0.3.31", "gpui", "language", "languages", @@ -7035,6 +7288,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" dependencies = [ "cfg-if", + "rayon", ] [[package]] @@ -7088,7 +7342,7 @@ name = "media" version = "0.1.0" dependencies = [ "anyhow", - "bindgen 0.70.1", + "bindgen", "core-foundation 0.9.4", "foreign-types 0.5.0", "metal", @@ -7107,14 +7361,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64" dependencies = [ - "rustix 0.38.35", + "rustix 0.38.40", ] [[package]] name = "memmap2" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" dependencies = [ "libc", ] @@ -7182,16 +7436,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" -dependencies = [ - "adler", - "simd-adler32", -] - [[package]] name = "miniz_oxide" version = "0.8.0" @@ -7199,6 +7443,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -7259,7 +7504,7 @@ dependencies = [ "collections", "ctor", "env_logger 0.11.5", - "futures 0.3.30", + "futures 0.3.31", "gpui", "itertools 0.13.0", "language", @@ -7293,12 +7538,12 @@ dependencies = [ "cfg_aliases 0.1.1", "codespan-reporting", "hexf-parse", - "indexmap 2.4.0", + "indexmap 2.6.0", "log", "rustc-hash 1.1.0", "spirv", "termcolor", - "thiserror", + "thiserror 1.0.69", "unicode-xid", ] @@ -7339,16 +7584,16 @@ dependencies = [ [[package]] name = "nbformat" -version = "0.5.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9ffb2ca556072f114bcaf2ca01dde7f1bc8a4946097dd804cb5a22d8af7d6df" +checksum = "aa6827a3881aa100bb2241cd2633b3c79474dbc93704f1f2cf5cc85064cda4be" dependencies = [ "anyhow", "chrono", "jupyter-serde", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", "uuid", ] @@ -7363,7 +7608,7 @@ dependencies = [ "log", "ndk-sys", "num_enum", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -7411,7 +7656,7 @@ dependencies = [ "async-trait", "async-watch", "async_zip", - "futures 0.3.30", + "futures 0.3.31", "http_client", "log", "paths", @@ -7423,7 +7668,6 @@ dependencies = [ "util", "walkdir", "which 6.0.3", - "windows 0.58.0", ] [[package]] @@ -7697,7 +7941,7 @@ version = "0.8.0-pre" source = "git+https://github.com/KillTheMule/nvim-rs?branch=master#69500bae73b8b3f02a05b7bee621a0d0e633da6c" dependencies = [ "async-trait", - "futures 0.3.30", + "futures 0.3.31", "log", "parity-tokio-ipc", "rmp", @@ -7718,13 +7962,13 @@ dependencies = [ [[package]] name = "object" -version = "0.36.4" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "crc32fast", - "hashbrown 0.14.5", - "indexmap 2.4.0", + "hashbrown 0.15.1", + "indexmap 2.6.0", "memchr", ] @@ -7756,7 +8000,7 @@ name = "ollama" version = "0.1.0" dependencies = [ "anyhow", - "futures 0.3.30", + "futures 0.3.31", "http_client", "schemars", "serde", @@ -7765,9 +8009,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "oo7" @@ -7777,7 +8021,7 @@ checksum = "8fc6ce4692fbfd044ce22ca07dcab1a30fa12432ca2aa5b1294eca50d3332a24" dependencies = [ "aes", "async-fs 2.1.2", - "async-io 2.3.4", + "async-io 2.4.0", "async-lock 3.4.0", "async-net 2.0.0", "blocking", @@ -7785,7 +8029,7 @@ dependencies = [ "cipher", "digest", "endi", - "futures-lite 2.3.0", + "futures-lite 2.5.0", "futures-util", "hkdf", "hmac", @@ -7810,9 +8054,9 @@ checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] name = "open" -version = "5.3.0" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3" +checksum = "3ecd52f0b8d15c40ce4820aa251ed5de032e5d91fab27f7db2f40d42a8bdf69c" dependencies = [ "is-wsl", "libc", @@ -7824,7 +8068,7 @@ name = "open_ai" version = "0.1.0" dependencies = [ "anyhow", - "futures 0.3.30", + "futures 0.3.31", "http_client", "schemars", "serde", @@ -7846,9 +8090,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ "bitflags 2.6.0", "cfg-if", @@ -7878,18 +8122,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.3.1+3.3.1" +version = "300.4.0+3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" +checksum = "a709e02f2b4aca747929cca5ed248880847c650233cf8b8cdc48f40aaf4898a6" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.103" +version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", @@ -8075,7 +8319,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9981e32fb75e004cc148f5fb70342f393830e0a4aa62e3cc93b50976218d42b6" dependencies = [ - "futures 0.3.30", + "futures 0.3.31", "libc", "log", "rand 0.7.3", @@ -8107,7 +8351,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.3", + "redox_syscall 0.5.7", "smallvec", "windows-targets 0.52.6", ] @@ -8207,20 +8451,20 @@ dependencies = [ [[package]] name = "pest" -version = "2.7.11" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" +checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" dependencies = [ "memchr", - "thiserror", + "thiserror 1.0.69", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.7.11" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a" +checksum = "d214365f632b123a47fd913301e14c946c61d1c183ee245fa76eb752e59a02dd" dependencies = [ "pest", "pest_generator", @@ -8228,9 +8472,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.11" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183" +checksum = "eb55586734301717aea2ac313f50b2eb8f60d2fc3dc01d190eefa2e625f60c4e" dependencies = [ "pest", "pest_meta", @@ -8241,9 +8485,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.11" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f" +checksum = "b75da2a70cf4d9cb76833c990ac9cd3923c9a8905a8929789ce347c84564d03d" dependencies = [ "once_cell", "pest", @@ -8617,7 +8861,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.4.0", + "indexmap 2.6.0", ] [[package]] @@ -8715,18 +8959,18 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pin-project" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", @@ -8735,9 +8979,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -8752,7 +8996,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", - "fastrand 2.1.1", + "fastrand 2.2.0", "futures-io", ] @@ -8789,9 +9033,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "plist" @@ -8800,7 +9044,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64 0.22.1", - "indexmap 2.4.0", + "indexmap 2.6.0", "quick-xml 0.32.0", "serde", "time", @@ -8808,9 +9052,9 @@ dependencies = [ [[package]] name = "plotters" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ "num-traits", "plotters-backend", @@ -8821,30 +9065,30 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] name = "plotters-svg" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ "plotters-backend", ] [[package]] name = "png" -version = "0.17.13" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" dependencies = [ "bitflags 1.3.2", "crc32fast", "fdeflate", "flate2", - "miniz_oxide 0.7.4", + "miniz_oxide", ] [[package]] @@ -8865,15 +9109,15 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.3" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi 0.4.0", "pin-project-lite", - "rustix 0.38.35", + "rustix 0.38.40", "tracing", "windows-sys 0.59.0", ] @@ -8892,13 +9136,13 @@ checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" dependencies = [ "atomic", "crossbeam-queue", - "futures 0.3.30", + "futures 0.3.31", "log", "parking_lot", "pin-project", "pollster", "static_assertions", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -8965,9 +9209,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.22" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" dependencies = [ "proc-macro2", "syn 2.0.87", @@ -8979,7 +9223,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ - "toml_edit 0.22.20", + "toml_edit 0.22.22", ] [[package]] @@ -9006,6 +9250,28 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "proc-macro2" version = "1.0.89" @@ -9049,7 +9315,7 @@ dependencies = [ "env_logger 0.11.5", "fancy-regex 0.14.0", "fs", - "futures 0.3.30", + "futures 0.3.31", "fuzzy", "git", "git2", @@ -9092,7 +9358,6 @@ dependencies = [ "url", "util", "which 6.0.3", - "windows 0.58.0", "worktree", ] @@ -9133,7 +9398,7 @@ version = "0.1.0" dependencies = [ "anyhow", "editor", - "futures 0.3.30", + "futures 0.3.31", "fuzzy", "gpui", "language", @@ -9161,7 +9426,7 @@ dependencies = [ "memchr", "parking_lot", "protobuf", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -9170,7 +9435,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "prost-derive", ] @@ -9180,7 +9445,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "heck 0.3.3", "itertools 0.10.5", "lazy_static", @@ -9213,7 +9478,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "prost", ] @@ -9236,9 +9501,9 @@ checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" [[package]] name = "psm" -version = "0.1.21" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" +checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810" dependencies = [ "cc", ] @@ -9318,72 +9583,58 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.34.0" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" dependencies = [ "memchr", ] -[[package]] -name = "quick_action_bar" -version = "0.1.0" -dependencies = [ - "assistant", - "editor", - "gpui", - "markdown_preview", - "picker", - "repl", - "search", - "settings", - "ui", - "util", - "workspace", - "zed_actions", -] - [[package]] name = "quinn" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" +checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "pin-project-lite", "quinn-proto", "quinn-udp", "rustc-hash 2.0.0", - "rustls 0.23.13", + "rustls 0.23.16", "socket2 0.5.7", - "thiserror", + "thiserror 2.0.3", "tokio", "tracing", ] [[package]] name = "quinn-proto" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" +checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", + "getrandom 0.2.15", "rand 0.8.5", - "ring", + "ring 0.17.8", "rustc-hash 2.0.0", - "rustls 0.23.13", + "rustls 0.23.16", + "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.3", "tinyvec", "tracing", + "web-time", ] [[package]] name = "quinn-udp" -version = "0.5.5" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" +checksum = "7d5a626c6807713b15cac82a6acaccd6043c9a5408c24baae07611fec3f243da" dependencies = [ + "cfg_aliases 0.2.1", "libc", "once_cell", "socket2 0.5.7", @@ -9513,22 +9764,23 @@ dependencies = [ "rand_chacha 0.3.1", "simd_helpers", "system-deps", - "thiserror", + "thiserror 1.0.69", "v_frame", "wasm-bindgen", ] [[package]] name = "ravif" -version = "0.11.10" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f0bfd976333248de2078d350bfdf182ff96e168a24d23d2436cef320dd4bdd" +checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6" dependencies = [ "avif-serialize", "imgref", "loop9", "quick-error", "rav1e", + "rayon", "rgb", ] @@ -9572,9 +9824,9 @@ dependencies = [ [[package]] name = "read-fonts" -version = "0.20.0" +version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c141b9980e1150201b2a3a32879001c8f975fe313ec3df5471a9b5c79a880cd" +checksum = "4a04b892cb6f91951f144c33321843790c8574c825aafdb16d815fd7183b5229" dependencies = [ "bytemuck", "font-types", @@ -9588,8 +9840,9 @@ dependencies = [ "auto_update", "dap", "editor", + "extension_host", "file_finder", - "futures 0.3.30", + "futures 0.3.31", "fuzzy", "gpui", "itertools 0.13.0", @@ -9613,6 +9866,7 @@ dependencies = [ "ui", "util", "workspace", + "zed_actions", ] [[package]] @@ -9626,18 +9880,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ "bitflags 2.6.0", ] @@ -9650,7 +9895,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -9675,14 +9920,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.6" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", ] [[package]] @@ -9696,13 +9941,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] @@ -9719,9 +9964,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "release_channel" @@ -9739,7 +9984,7 @@ dependencies = [ "async-trait", "collections", "fs", - "futures 0.3.30", + "futures 0.3.31", "gpui", "itertools 0.13.0", "log", @@ -9753,8 +9998,9 @@ dependencies = [ "shlex", "smol", "tempfile", - "thiserror", + "thiserror 1.0.69", "util", + "which 6.0.3", ] [[package]] @@ -9770,14 +10016,17 @@ dependencies = [ "client", "clock", "env_logger 0.11.5", + "extension", + "extension_host", "fork", "fs", - "futures 0.3.30", + "futures 0.3.31", "git", "git_hosting_providers", "gpui", "http_client", "language", + "language_extension", "languages", "libc", "log", @@ -9826,11 +10075,14 @@ dependencies = [ "editor", "env_logger 0.11.5", "feature_flags", - "futures 0.3.30", + "file_icons", + "futures 0.3.31", "gpui", "http_client", "image", "indoc", + "jupyter-protocol", + "jupyter-websocket-client", "language", "languages", "log", @@ -9855,7 +10107,6 @@ dependencies = [ "ui", "util", "uuid", - "windows 0.58.0", "workspace", ] @@ -9866,7 +10117,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "base64 0.21.7", - "bytes 1.7.2", + "bytes 1.8.0", "encoding_rs", "futures-core", "futures-util", @@ -9905,7 +10156,7 @@ version = "0.12.8" source = "git+https://github.com/zed-industries/reqwest.git?rev=fd110f6998da16bbca97b6dddda9be7827c50e29#fd110f6998da16bbca97b6dddda9be7827c50e29" dependencies = [ "base64 0.22.1", - "bytes 1.7.2", + "bytes 1.8.0", "encoding_rs", "futures-core", "futures-util", @@ -9913,7 +10164,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-rustls 0.27.3", "hyper-util", "ipnet", @@ -9924,9 +10175,9 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.13", + "rustls 0.23.16", "rustls-native-certs 0.8.0", - "rustls-pemfile 2.1.3", + "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", "serde_json", @@ -9951,8 +10202,8 @@ name = "reqwest_client" version = "0.1.0" dependencies = [ "anyhow", - "bytes 1.7.2", - "futures 0.3.30", + "bytes 1.8.0", + "futures 0.3.31", "gpui", "http_client", "log", @@ -9989,9 +10240,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.49" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cd5a1e95672f201913966f39baf355b53b5d92833431847295ae0346a5b939" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" dependencies = [ "bytemuck", ] @@ -10000,7 +10251,7 @@ dependencies = [ name = "rich_text" version = "0.1.0" dependencies = [ - "futures 0.3.30", + "futures 0.3.31", "gpui", "language", "linkify", @@ -10010,6 +10261,21 @@ dependencies = [ "util", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + [[package]] name = "ring" version = "0.17.8" @@ -10020,8 +10286,8 @@ dependencies = [ "cfg-if", "getrandom 0.2.15", "libc", - "spin", - "untrusted", + "spin 0.9.8", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -10033,7 +10299,7 @@ checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" dependencies = [ "bitvec", "bytecheck", - "bytes 1.7.2", + "bytes 1.8.0", "hashbrown 0.12.3", "ptr_meta", "rend", @@ -10083,7 +10349,7 @@ checksum = "6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb" dependencies = [ "cpal", "hound", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -10115,12 +10381,12 @@ name = "rpc" version = "0.1.0" dependencies = [ "anyhow", - "async-tungstenite", + "async-tungstenite 0.28.0", "base64 0.22.1", "chrono", "collections", "env_logger 0.11.5", - "futures 0.3.30", + "futures 0.3.31", "gpui", "parking_lot", "proto", @@ -10157,23 +10423,23 @@ dependencies = [ [[package]] name = "runtimelib" -version = "0.19.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe23ba9967355bbb1be2fb9a8e51bd239ffdf9c791fad5a9b765122ee2bde2e4" +checksum = "b3a8ab675beb5cf25c28f9c6ddb8f47bcf73b43872797e6ab6157865f44d1e19" dependencies = [ "anyhow", "async-dispatcher", "async-std", "base64 0.22.1", - "bytes 1.7.2", + "bytes 1.8.0", "chrono", "data-encoding", "dirs 5.0.1", - "futures 0.3.30", + "futures 0.3.31", "glob", + "jupyter-protocol", "jupyter-serde", - "rand 0.8.5", - "ring", + "ring 0.17.8", "serde", "serde_json", "shellexpand 3.1.0", @@ -10225,7 +10491,7 @@ checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" dependencies = [ "arrayvec", "borsh", - "bytes 1.7.2", + "bytes 1.8.0", "num-traits", "rand 0.8.5", "rkyv", @@ -10276,9 +10542,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.35" +version = "0.38.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f" +checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" dependencies = [ "bitflags 2.6.0", "errno 0.3.9", @@ -10297,7 +10563,19 @@ checksum = "a25c3aad9fc1424eb82c88087789a7d938e1829724f3e4043163baf0d13cfc12" dependencies = [ "errno 0.3.9", "libc", - "rustix 0.38.35", + "rustix 0.38.40", +] + +[[package]] +name = "rustls" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +dependencies = [ + "log", + "ring 0.16.20", + "sct", + "webpki", ] [[package]] @@ -10307,19 +10585,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", - "ring", + "ring 0.17.8", "rustls-webpki 0.101.7", "sct", ] [[package]] name = "rustls" -version = "0.23.13" +version = "0.23.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" +checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" dependencies = [ "once_cell", - "ring", + "ring 0.17.8", "rustls-pki-types", "rustls-webpki 0.102.8", "subtle", @@ -10345,7 +10623,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a" dependencies = [ "openssl-probe", - "rustls-pemfile 2.1.3", + "rustls-pemfile 2.2.0", "rustls-pki-types", "schannel", "security-framework", @@ -10362,19 +10640,21 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.3" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +dependencies = [ + "web-time", +] [[package]] name = "rustls-webpki" @@ -10382,8 +10662,8 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring", - "untrusted", + "ring 0.17.8", + "untrusted 0.9.0", ] [[package]] @@ -10392,16 +10672,16 @@ version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ - "ring", + "ring 0.17.8", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "rustybuzz" @@ -10446,11 +10726,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -10508,18 +10788,18 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring", - "untrusted", + "ring 0.17.8", + "untrusted 0.9.0", ] [[package]] name = "sea-bae" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bd3534a9978d0aa7edd2808dc1f8f31c4d0ecd31ddf71d997b3c98e9f3c9114" +checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25" dependencies = [ "heck 0.4.1", - "proc-macro-error", + "proc-macro-error2", "proc-macro2", "quote", "syn 2.0.87", @@ -10535,7 +10815,7 @@ dependencies = [ "async-trait", "bigdecimal", "chrono", - "futures 0.3.30", + "futures 0.3.31", "log", "ouroboros", "rust_decimal", @@ -10546,7 +10826,7 @@ dependencies = [ "serde_json", "sqlx", "strum 0.26.3", - "thiserror", + "thiserror 1.0.69", "time", "tracing", "url", @@ -10555,9 +10835,9 @@ dependencies = [ [[package]] name = "sea-orm-macros" -version = "1.1.0-rc.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07aadcb2ee9fad78a3bf74f6430ba94865ab4d8ad237f978e99dafa97ee0df57" +checksum = "3a239e3bb1b566ad4ec2654d0d193d6ceddfd733487edc9c21a64d214c773910" dependencies = [ "heck 0.4.1", "proc-macro2", @@ -10569,13 +10849,12 @@ dependencies = [ [[package]] name = "sea-query" -version = "0.32.0-rc.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fba498acd58ce434669f273505cd07737065472eb541c3f813c7f4ce33993f5" +checksum = "ff504d13b5e4b52fffcf2fb203d0352a5722fa5151696db768933e41e1e591bb" dependencies = [ "bigdecimal", "chrono", - "educe", "inherent", "ordered-float 3.9.2", "rust_decimal", @@ -10586,9 +10865,9 @@ dependencies = [ [[package]] name = "sea-query-binder" -version = "0.7.0-rc.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc3296903e60ddc7c9f4601cd6ef31a4b1584bf22480587e00b9ef743071b57" +checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608" dependencies = [ "bigdecimal", "chrono", @@ -10628,7 +10907,7 @@ dependencies = [ "client", "collections", "editor", - "futures 0.3.30", + "futures 0.3.31", "gpui", "language", "menu", @@ -10673,9 +10952,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" dependencies = [ "core-foundation-sys", "libc", @@ -10700,7 +10979,7 @@ dependencies = [ "env_logger 0.11.5", "feature_flags", "fs", - "futures 0.3.30", + "futures 0.3.31", "futures-batch", "gpui", "heed", @@ -10747,18 +11026,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.209" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.209" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", @@ -10798,11 +11077,11 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ - "indexmap 2.4.0", + "indexmap 2.6.0", "itoa", "memchr", "ryu", @@ -10811,12 +11090,13 @@ dependencies = [ [[package]] name = "serde_json_lenient" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d0bae483150302560d7cb52e7932f39b69a6fbdd099e48d33ef060a8c9c078" +checksum = "2bf0c7e21364d0e199dd2f6c339ca18d6fca75b69458a247e8b27ff1c92f5b86" dependencies = [ - "indexmap 2.4.0", + "indexmap 2.6.0", "itoa", + "memchr", "ryu", "serde", ] @@ -10839,7 +11119,7 @@ checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" dependencies = [ "percent-encoding", "serde", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -10850,7 +11130,7 @@ checksum = "8cac3f1e2ca2fe333923a1ae72caca910b98ed0630bb35ef6f8c8517d6e81afa" dependencies = [ "percent-encoding", "serde", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -10866,9 +11146,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -10904,7 +11184,7 @@ dependencies = [ "collections", "ec4rs", "fs", - "futures 0.3.30", + "futures 0.3.31", "gpui", "indoc", "log", @@ -11061,9 +11341,9 @@ dependencies = [ [[package]] name = "simdutf8" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "similar" @@ -11079,7 +11359,7 @@ checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" dependencies = [ "num-bigint", "num-traits", - "thiserror", + "thiserror 1.0.69", "time", ] @@ -11117,9 +11397,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "skrifa" -version = "0.20.0" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abea4738067b1e628c6ce28b2c216c19e9ea95715cdb332680e821c3bec2ef23" +checksum = "8e1c44ad1f6c5bdd4eefed8326711b7dbda9ea45dfd36068c427d332aa382cbe" dependencies = [ "bytemuck", "read-fonts", @@ -11216,8 +11496,9 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", + "extension", "fs", - "futures 0.3.30", + "futures 0.3.31", "gpui", "parking_lot", "paths", @@ -11263,13 +11544,19 @@ dependencies = [ [[package]] name = "spdx" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47317bbaf63785b53861e1ae2d11b80d6b624211d42cb20efcd210ee6f8a14bc" +checksum = "bae30cc7bfe3656d60ee99bf6836f472b0c53dddcbf335e253329abb16e535a2" dependencies = [ "smallvec", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "spin" version = "0.9.8" @@ -11320,7 +11607,7 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", - "futures 0.3.30", + "futures 0.3.31", "indoc", "libsqlite3-sys", "parking_lot", @@ -11373,7 +11660,7 @@ dependencies = [ "atoi", "bigdecimal", "byteorder", - "bytes 1.7.2", + "bytes 1.8.0", "chrono", "crc", "crossbeam-queue", @@ -11387,7 +11674,7 @@ dependencies = [ "hashbrown 0.14.5", "hashlink 0.9.1", "hex", - "indexmap 2.4.0", + "indexmap 2.6.0", "log", "memchr", "once_cell", @@ -11401,7 +11688,7 @@ dependencies = [ "sha2", "smallvec", "sqlformat", - "thiserror", + "thiserror 1.0.69", "time", "tokio", "tokio-stream", @@ -11461,7 +11748,7 @@ dependencies = [ "bigdecimal", "bitflags 2.6.0", "byteorder", - "bytes 1.7.2", + "bytes 1.8.0", "chrono", "crc", "digest", @@ -11490,7 +11777,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.69", "time", "tracing", "uuid", @@ -11534,7 +11821,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.69", "time", "tracing", "uuid", @@ -11724,9 +12011,10 @@ dependencies = [ "collections", "editor", "env_logger 0.11.5", - "futures 0.3.30", + "futures 0.3.31", "gpui", "http_client", + "inline_completion", "language", "log", "postage", @@ -11741,7 +12029,6 @@ dependencies = [ "ui", "unicode-segmentation", "util", - "windows 0.58.0", ] [[package]] @@ -11749,7 +12036,7 @@ name = "supermaven_api" version = "0.1.0" dependencies = [ "anyhow", - "futures 0.3.30", + "futures 0.3.31", "http_client", "paths", "serde", @@ -11759,15 +12046,15 @@ dependencies = [ [[package]] name = "sval" -version = "2.13.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53eb957fbc79a55306d5d25d87daf3627bc3800681491cda0709eef36c748bfe" +checksum = "f6dc0f9830c49db20e73273ffae9b5240f63c42e515af1da1fceefb69fceafd8" [[package]] name = "sval_buffer" -version = "2.13.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96e860aef60e9cbf37888d4953a13445abf523c534640d1f6174d310917c410d" +checksum = "429922f7ad43c0ef8fd7309e14d750e38899e32eb7e8da656ea169dd28ee212f" dependencies = [ "sval", "sval_ref", @@ -11775,18 +12062,18 @@ dependencies = [ [[package]] name = "sval_dynamic" -version = "2.13.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea3f2b07929a1127d204ed7cb3905049381708245727680e9139dac317ed556f" +checksum = "68f16ff5d839396c11a30019b659b0976348f3803db0626f736764c473b50ff4" dependencies = [ "sval", ] [[package]] name = "sval_fmt" -version = "2.13.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e188677497de274a1367c4bda15bd2296de4070d91729aac8f0a09c1abf64d" +checksum = "c01c27a80b6151b0557f9ccbe89c11db571dc5f68113690c1e028d7e974bae94" dependencies = [ "itoa", "ryu", @@ -11795,9 +12082,9 @@ dependencies = [ [[package]] name = "sval_json" -version = "2.13.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f456c07dae652744781f2245d5e3b78e6a9ebad70790ac11eb15dbdbce5282" +checksum = "0deef63c70da622b2a8069d8600cf4b05396459e665862e7bdb290fd6cf3f155" dependencies = [ "itoa", "ryu", @@ -11806,9 +12093,9 @@ dependencies = [ [[package]] name = "sval_nested" -version = "2.13.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "886feb24709f0476baaebbf9ac10671a50163caa7e439d7a7beb7f6d81d0a6fb" +checksum = "a39ce5976ae1feb814c35d290cf7cf8cd4f045782fe1548d6bc32e21f6156e9f" dependencies = [ "sval", "sval_buffer", @@ -11817,18 +12104,18 @@ dependencies = [ [[package]] name = "sval_ref" -version = "2.13.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be2e7fc517d778f44f8cb64140afa36010999565528d48985f55e64d45f369ce" +checksum = "bb7c6ee3751795a728bc9316a092023529ffea1783499afbc5c66f5fabebb1fa" dependencies = [ "sval", ] [[package]] name = "sval_serde" -version = "2.13.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79bf66549a997ff35cd2114a27ac4b0c2843280f2cfa84b240d169ecaa0add46" +checksum = "2a5572d0321b68109a343634e3a5d576bf131b82180c6c442dee06349dfc652a" dependencies = [ "serde", "sval", @@ -11837,9 +12124,9 @@ dependencies = [ [[package]] name = "svg_fmt" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20e16a0f46cf5fd675563ef54f26e83e20f2366bcf027bcb3cc3ed2b98aaf2ca" +checksum = "ce5d813d71d82c4cbc1742135004e4a79fd870214c155443451c139c9470a0aa" [[package]] name = "svgtypes" @@ -11853,9 +12140,9 @@ dependencies = [ [[package]] name = "swash" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93cdc334a50fcc2aa3f04761af3b28196280a6aaadb1ef11215c478ae32615ac" +checksum = "cbd59f3f359ddd2c95af4758c18270eddd9c730dde98598023cdabff472c2ca2" dependencies = [ "skrifa", "yazi", @@ -11884,18 +12171,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "syn_derive" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.87", -] - [[package]] name = "sync_wrapper" version = "0.1.2" @@ -11920,6 +12195,17 @@ dependencies = [ "crossbeam-queue", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "sys-locale" version = "0.3.2" @@ -11940,7 +12226,7 @@ dependencies = [ "memchr", "ntapi", "rayon", - "windows 0.54.0", + "windows 0.57.0", ] [[package]] @@ -12009,7 +12295,7 @@ dependencies = [ "cap-std", "fd-lock", "io-lifetimes 2.0.3", - "rustix 0.38.35", + "rustix 0.38.40", "windows-sys 0.52.0", "winx", ] @@ -12074,7 +12360,7 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", - "futures 0.3.30", + "futures 0.3.31", "gpui", "hex", "parking_lot", @@ -12110,6 +12396,7 @@ dependencies = [ "ui", "util", "workspace", + "zed_actions", ] [[package]] @@ -12122,14 +12409,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", - "fastrand 2.1.1", + "fastrand 2.2.0", "once_cell", - "rustix 0.38.35", + "rustix 0.38.40", "windows-sys 0.59.0", ] @@ -12161,7 +12448,7 @@ dependencies = [ "anyhow", "collections", "dirs 4.0.0", - "futures 0.3.30", + "futures 0.3.31", "gpui", "libc", "rand 0.8.5", @@ -12175,7 +12462,7 @@ dependencies = [ "sysinfo", "task", "theme", - "thiserror", + "thiserror 1.0.69", "util", "windows 0.58.0", ] @@ -12186,7 +12473,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" dependencies = [ - "rustix 0.38.35", + "rustix 0.38.40", "windows-sys 0.59.0", ] @@ -12195,12 +12482,13 @@ name = "terminal_view" version = "0.1.0" dependencies = [ "anyhow", + "breadcrumbs", "client", "collections", "db", "dirs 4.0.0", "editor", - "futures 0.3.30", + "futures 0.3.31", "gpui", "itertools 0.13.0", "language", @@ -12213,7 +12501,6 @@ dependencies = [ "shellexpand 2.1.2", "smol", "task", - "tasks_ui", "terminal", "theme", "ui", @@ -12252,7 +12539,7 @@ dependencies = [ "collections", "derive_more", "fs", - "futures 0.3.30", + "futures 0.3.31", "gpui", "indexmap 1.9.3", "log", @@ -12271,6 +12558,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "theme_extension" +version = "0.1.0" +dependencies = [ + "anyhow", + "extension", + "fs", + "gpui", + "theme", +] + [[package]] name = "theme_importer" version = "0.1.0" @@ -12308,6 +12606,7 @@ dependencies = [ "ui", "util", "workspace", + "zed_actions", ] [[package]] @@ -12316,7 +12615,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +dependencies = [ + "thiserror-impl 2.0.3", ] [[package]] @@ -12330,6 +12638,17 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "thiserror-impl" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -12457,6 +12776,16 @@ dependencies = [ "url", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -12490,17 +12819,12 @@ dependencies = [ "call", "client", "collections", - "command_palette", - "editor", - "extensions_ui", "feature_flags", - "feedback", "gpui", "http_client", "notifications", "pretty_assertions", "project", - "recent_projects", "remote", "rpc", "serde", @@ -12508,11 +12832,9 @@ dependencies = [ "smallvec", "story", "theme", - "theme_selector", "tree-sitter-md", "ui", "util", - "vcs_menu", "windows 0.58.0", "workspace", "zed_actions", @@ -12520,12 +12842,12 @@ dependencies = [ [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ "backtrace", - "bytes 1.7.2", + "bytes 1.8.0", "libc", "mio 1.0.2", "parking_lot", @@ -12584,7 +12906,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.13", + "rustls 0.23.16", "rustls-pki-types", "tokio", ] @@ -12598,15 +12920,15 @@ dependencies = [ "either", "futures-io", "futures-util", - "thiserror", + "thiserror 1.0.69", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" dependencies = [ "futures-core", "pin-project-lite", @@ -12643,7 +12965,7 @@ version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "futures-core", "futures-io", "futures-sink", @@ -12681,7 +13003,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.20", + "toml_edit 0.22.22", ] [[package]] @@ -12699,7 +13021,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.4.0", + "indexmap 2.6.0", "serde", "serde_spanned", "toml_datetime", @@ -12708,15 +13030,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap 2.4.0", + "indexmap 2.6.0", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.18", + "winnow 0.6.20", ] [[package]] @@ -12763,7 +13085,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" dependencies = [ "bitflags 1.3.2", - "bytes 1.7.2", + "bytes 1.8.0", "futures-core", "futures-util", "http 0.2.12", @@ -12781,7 +13103,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" dependencies = [ "bitflags 2.6.0", - "bytes 1.7.2", + "bytes 1.8.0", "futures-core", "futures-util", "http 0.2.12", @@ -12882,22 +13204,22 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.23.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20f4cd3642c47a85052a887d86704f4eac272969f61b686bdd3f772122aabaff" +checksum = "0203df02a3b6dd63575cc1d6e609edc2181c9a11867a271b25cfd2abff3ec5ca" dependencies = [ "cc", "regex", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", "tree-sitter-language", "wasmtime-c-api-impl", ] [[package]] name = "tree-sitter-bash" -version = "0.23.1" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aa5e1c6bd02c0053f3f68edcf5d8866b38a8640584279e30fca88149ce14dda" +checksum = "329a4d48623ac337d42b1df84e81a1c9dbb2946907c102ca72db158c1964a52e" dependencies = [ "cc", "tree-sitter-language", @@ -12915,9 +13237,9 @@ dependencies = [ [[package]] name = "tree-sitter-cpp" -version = "0.23.1" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d67e862242878d6ee50e1e5814f267ee3eea0168aea2cdbd700ccfb4c74b6d3" +checksum = "df2196ea9d47b4ab4a31b9297eaa5a5d19a0b121dceb9f118f6790ad0ab94743" dependencies = [ "cc", "tree-sitter-language", @@ -12925,9 +13247,9 @@ dependencies = [ [[package]] name = "tree-sitter-css" -version = "0.23.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d0018d6b1692a806f9cddaa1e5616951fd58840c39a0b21401b55ab3df12292" +checksum = "25435a275adb3226b6fddab891bbc50d1a500774a44ceb97022a39666ccda75d" dependencies = [ "cc", "tree-sitter-language", @@ -12955,9 +13277,9 @@ dependencies = [ [[package]] name = "tree-sitter-embedded-template" -version = "0.23.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9644d7586ebe850c84037ee2f4804dda4a9348eef053be6b1e0d7712342a2495" +checksum = "790063ef14e5b67556abc0b3be0ed863fb41d65ee791cf8c0b20eb42a1fa46af" dependencies = [ "cc", "tree-sitter-language", @@ -12965,9 +13287,9 @@ dependencies = [ [[package]] name = "tree-sitter-go" -version = "0.23.1" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf57626e4c9b6d6efaf8a8d5ee1241c5f178ae7bfdf693713ae6a774f01424e" +checksum = "dc4ee804a89f5c0e606b0b20579c86afc7cd0174aebd45c33b6b9c6237bcd97d" dependencies = [ "cc", "tree-sitter-language", @@ -13012,9 +13334,9 @@ dependencies = [ [[package]] name = "tree-sitter-jsdoc" -version = "0.23.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c4049eb0ad690e34e5f63640f75ce12a2ff8ba18344d0a13926805b139c0c8" +checksum = "a3862dfcb1038fc5e7812d7df14190afdeb7e1415288fd5f51f58395f8cb0faf" dependencies = [ "cc", "tree-sitter-language", @@ -13032,9 +13354,9 @@ dependencies = [ [[package]] name = "tree-sitter-language" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2545046bd1473dac6c626659cc2567c6c0ff302fc8b84a56c4243378276f7f57" +checksum = "e8ddffe35a0e5eeeadf13ff7350af564c6e73993a24db62caee1822b185c2600" [[package]] name = "tree-sitter-md" @@ -13047,9 +13369,9 @@ dependencies = [ [[package]] name = "tree-sitter-python" -version = "0.23.2" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65661b1a3e24139e2e54207e47d910ab07e28790d78efc7d5dc3a11ce2a110eb" +checksum = "2416de7eea3f2e1bd53c250f2d3f3394fc77f78497680f37f4b87918b8d752e3" dependencies = [ "cc", "tree-sitter-language", @@ -13067,9 +13389,9 @@ dependencies = [ [[package]] name = "tree-sitter-ruby" -version = "0.23.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ec5ee842e27791e0adffa0b2a177614de51d2a26e5c7e84d014ed7f097e5ed0" +checksum = "be0484ea4ef6bb9c575b4fdabde7e31340a8d2dbc7d52b321ac83da703249f95" dependencies = [ "cc", "tree-sitter-language", @@ -13077,9 +13399,9 @@ dependencies = [ [[package]] name = "tree-sitter-rust" -version = "0.23.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cffbbcb780348fbae8395742ae5b34c1fd794e4085d43aac9f259387f9a84dc8" +checksum = "137ff3de3cc8a98302d048963459ead91135d4a1b423f09d25028b847ec3d3e3" dependencies = [ "cc", "tree-sitter-language", @@ -13087,9 +13409,9 @@ dependencies = [ [[package]] name = "tree-sitter-typescript" -version = "0.23.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aecf1585ae2a9dddc2b1d4c0e2140b2ec9876e2a25fd79de47fcf7dae0384685" +checksum = "6c5f76ed8d947a75cc446d5fccd8b602ebf0cde64ccf2ffa434d873d7a575eff" dependencies = [ "cc", "tree-sitter-language", @@ -13118,19 +13440,38 @@ checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" [[package]] name = "tungstenite" -version = "0.20.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +checksum = "15fba1a6d6bb030745759a9a2a588bfe8490fc8b4751a277db3a0be1c9ebbf67" dependencies = [ "byteorder", - "bytes 1.7.2", + "bytes 1.8.0", "data-encoding", "http 0.2.12", "httparse", "log", "rand 0.8.5", "sha1", - "thiserror", + "thiserror 1.0.69", + "url", + "utf-8", +] + +[[package]] +name = "tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes 1.8.0", + "data-encoding", + "http 0.2.12", + "httparse", + "log", + "rand 0.8.5", + "sha1", + "thiserror 1.0.69", "url", "utf-8", ] @@ -13142,14 +13483,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" dependencies = [ "byteorder", - "bytes 1.7.2", + "bytes 1.8.0", "data-encoding", "http 1.1.0", "httparse", "log", "rand 0.8.5", "sha1", - "thiserror", + "thiserror 1.0.69", "url", "utf-8", ] @@ -13161,14 +13502,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" dependencies = [ "byteorder", - "bytes 1.7.2", + "bytes 1.8.0", "data-encoding", "http 1.1.0", "httparse", "log", "rand 0.8.5", "sha1", - "thiserror", + "thiserror 1.0.69", "utf-8", ] @@ -13186,9 +13527,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "ucd-trie" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "uds_windows" @@ -13242,18 +13583,15 @@ dependencies = [ [[package]] name = "unicase" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-bidi-mirroring" @@ -13269,9 +13607,9 @@ checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-linebreak" @@ -13281,18 +13619,18 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" [[package]] name = "unicode-script" @@ -13302,21 +13640,21 @@ checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" [[package]] name = "unicode-segmentation" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-xid" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "unicode_categories" @@ -13330,6 +13668,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -13338,9 +13682,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" dependencies = [ "form_urlencoded", "idna", @@ -13382,6 +13726,18 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -13396,7 +13752,7 @@ dependencies = [ "async-fs 1.6.0", "collections", "dirs 4.0.0", - "futures 0.3.30", + "futures 0.3.31", "futures-lite 1.13.0", "git2", "globset", @@ -13406,6 +13762,7 @@ dependencies = [ "rust-embed", "serde", "serde_json", + "smol", "take-until", "tempfile", "tendril", @@ -13414,9 +13771,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom 0.2.15", "serde", @@ -13442,9 +13799,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" +checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" dependencies = [ "value-bag-serde1", "value-bag-sval2", @@ -13452,9 +13809,9 @@ dependencies = [ [[package]] name = "value-bag-serde1" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccacf50c5cb077a9abb723c5bcb5e0754c1a433f1e1de89edc328e2760b6328b" +checksum = "4bb773bd36fd59c7ca6e336c94454d9c66386416734817927ac93d81cb3c5b0b" dependencies = [ "erased-serde", "serde", @@ -13463,9 +13820,9 @@ dependencies = [ [[package]] name = "value-bag-sval2" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1785bae486022dfb9703915d42287dcb284c1ee37bd1080eeba78cc04721285b" +checksum = "53a916a702cac43a88694c97657d449775667bcd14b70419441d05b7fea4a83a" dependencies = [ "sval", "sval_buffer", @@ -13495,6 +13852,7 @@ dependencies = [ "ui", "util", "workspace", + "zed_actions", ] [[package]] @@ -13520,7 +13878,7 @@ dependencies = [ "command_palette", "command_palette_hooks", "editor", - "futures 0.3.30", + "futures 0.3.31", "gpui", "indoc", "itertools 0.13.0", @@ -13541,10 +13899,20 @@ dependencies = [ "tokio", "ui", "util", + "vim_mode_setting", "workspace", "zed_actions", ] +[[package]] +name = "vim_mode_setting" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui", + "settings", +] + [[package]] name = "vscode_theme" version = "0.2.0" @@ -13635,7 +14003,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4378d202ff965b011c64817db11d5829506d3404edeadb61f190d111da3f231c" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "futures-channel", "futures-util", "headers", @@ -13677,9 +14045,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", "once_cell", @@ -13688,9 +14056,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", @@ -13703,9 +14071,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.43" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" dependencies = [ "cfg-if", "js-sys", @@ -13715,9 +14083,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -13725,9 +14093,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", @@ -13738,9 +14106,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "wasm-encoder" @@ -13767,7 +14135,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fd83062c17b9f4985d438603cde0a5e8c5c8198201a6937f778b607924c7da2" dependencies = [ "anyhow", - "indexmap 2.4.0", + "indexmap 2.6.0", "serde", "serde_derive", "serde_json", @@ -13778,9 +14146,9 @@ dependencies = [ [[package]] name = "wasm-streams" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" dependencies = [ "futures-util", "js-sys", @@ -13796,7 +14164,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708" dependencies = [ "bitflags 2.6.0", - "indexmap 2.4.0", + "indexmap 2.6.0", "semver", ] @@ -13809,7 +14177,7 @@ dependencies = [ "ahash 0.8.11", "bitflags 2.6.0", "hashbrown 0.14.5", - "indexmap 2.4.0", + "indexmap 2.6.0", "semver", "serde", ] @@ -13839,7 +14207,7 @@ dependencies = [ "cfg-if", "encoding_rs", "hashbrown 0.14.5", - "indexmap 2.4.0", + "indexmap 2.6.0", "libc", "libm", "log", @@ -13850,7 +14218,7 @@ dependencies = [ "paste", "postcard", "psm", - "rustix 0.38.35", + "rustix 0.38.40", "semver", "serde", "serde_derive", @@ -13882,9 +14250,9 @@ dependencies = [ [[package]] name = "wasmtime-c-api-impl" -version = "24.0.0" +version = "24.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765e302e7d9125e614aaeec3ad6b6083605393004eca00214106a4ff6b47fc58" +checksum = "4e038dd412700174019867608617127e7cc4f113f764dd10e7488dbf5f47b191" dependencies = [ "anyhow", "log", @@ -13896,9 +14264,9 @@ dependencies = [ [[package]] name = "wasmtime-c-api-macros" -version = "24.0.0" +version = "24.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d09d02eaa84aa2de5babee7b0296557ad6e4903bb10aa8d135e393e753a43d6" +checksum = "bde0ca2263811d980ab676bcb2a190c990737f58969a908976101ad208149a17" dependencies = [ "proc-macro2", "quote", @@ -13943,7 +14311,7 @@ dependencies = [ "log", "object", "target-lexicon", - "thiserror", + "thiserror 1.0.69", "wasmparser 0.215.0", "wasmtime-environ", "wasmtime-versioned-export-macros", @@ -13960,7 +14328,7 @@ dependencies = [ "cranelift-bitset", "cranelift-entity", "gimli 0.29.0", - "indexmap 2.4.0", + "indexmap 2.6.0", "log", "object", "postcard", @@ -13985,7 +14353,7 @@ dependencies = [ "anyhow", "cc", "cfg-if", - "rustix 0.38.35", + "rustix 0.38.40", "wasmtime-asm-macros", "wasmtime-versioned-export-macros", "windows-sys 0.52.0", @@ -14036,27 +14404,27 @@ dependencies = [ [[package]] name = "wasmtime-wasi" -version = "24.0.1" +version = "24.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda03f5bfd5c4cc09f75c7e44846663f25f2c48a2d688fbfb5c7a33af6cf34f5" +checksum = "f88f94e393084426f5055d57ce7ae6346ae623783ee6792f411282d6b9e1e5c3" dependencies = [ "anyhow", "async-trait", "bitflags 2.6.0", - "bytes 1.7.2", + "bytes 1.8.0", "cap-fs-ext", "cap-net-ext", "cap-rand", "cap-std", "cap-time-ext", "fs-set-times", - "futures 0.3.30", + "futures 0.3.31", "io-extras", "io-lifetimes 2.0.3", "once_cell", - "rustix 0.38.35", + "rustix 0.38.40", "system-interface", - "thiserror", + "thiserror 1.0.69", "tokio", "tracing", "url", @@ -14090,7 +14458,7 @@ checksum = "c58b085b2d330e5057dddd31f3ca527569b90fcdd35f6d373420c304927a5190" dependencies = [ "anyhow", "heck 0.4.1", - "indexmap 2.4.0", + "indexmap 2.6.0", "wit-parser 0.215.0", ] @@ -14105,13 +14473,13 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993" +checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" dependencies = [ "cc", "downcast-rs", - "rustix 0.38.35", + "rustix 0.38.40", "scoped-tls", "smallvec", "wayland-sys", @@ -14119,23 +14487,23 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.5" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943" +checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280" dependencies = [ "bitflags 2.6.0", - "rustix 0.38.35", + "rustix 0.38.40", "wayland-backend", "wayland-scanner", ] [[package]] name = "wayland-cursor" -version = "0.31.5" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef9489a8df197ebf3a8ce8a7a7f0a2320035c3743f3c1bd0bdbccf07ce64f95" +checksum = "32b08bc3aafdb0035e7fe0fdf17ba0c09c268732707dca4ae098f60cb28c9e4c" dependencies = [ - "rustix 0.38.35", + "rustix 0.38.40", "wayland-client", "xcursor", ] @@ -14167,20 +14535,20 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.4" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6" +checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" dependencies = [ "proc-macro2", - "quick-xml 0.34.0", + "quick-xml 0.36.2", "quote", ] [[package]] name = "wayland-sys" -version = "0.31.4" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148" +checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09" dependencies = [ "dlib", "log", @@ -14190,9 +14558,19 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +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", @@ -14204,8 +14582,8 @@ version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" dependencies = [ - "ring", - "untrusted", + "ring 0.17.8", + "untrusted 0.9.0", ] [[package]] @@ -14235,22 +14613,20 @@ version = "0.1.0" dependencies = [ "anyhow", "client", + "copilot", "db", "editor", - "extensions_ui", "fuzzy", "gpui", - "inline_completion_button", "install_cli", "picker", "project", "schemars", "serde", "settings", - "theme_selector", "ui", "util", - "vim", + "vim_mode_setting", "workspace", "zed_actions", ] @@ -14264,7 +14640,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix 0.38.35", + "rustix 0.38.40", ] [[package]] @@ -14275,30 +14651,30 @@ checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" dependencies = [ "either", "home", - "rustix 0.38.35", + "rustix 0.38.40", "winsafe", ] [[package]] name = "whoami" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" dependencies = [ - "redox_syscall 0.4.1", + "redox_syscall 0.5.7", "wasite", ] [[package]] name = "wiggle" -version = "24.0.1" +version = "24.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3b31bd2b4d2d82a4b747b8dbc45f566214214a4ffdc5690429a73bc221dc8a" +checksum = "c72a4c92952216582f55eab27819a1fe8d3c54b292b7b8e5f849b23bfed96e78" dependencies = [ "anyhow", "async-trait", "bitflags 2.6.0", - "thiserror", + "thiserror 1.0.69", "tracing", "wasmtime", "wiggle-macro", @@ -14306,9 +14682,9 @@ dependencies = [ [[package]] name = "wiggle-generate" -version = "24.0.1" +version = "24.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2c6136b195fc12067aa9d4e7a5baf118729394df7bc7cbf8c63119bc9f2a7cd" +checksum = "cb744fb938a9fc38207838829b4a43831c1de499e3526eaea71deeff4d9cbb83" dependencies = [ "anyhow", "heck 0.4.1", @@ -14321,9 +14697,9 @@ dependencies = [ [[package]] name = "wiggle-macro" -version = "24.0.1" +version = "24.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a41eaceee468da976ac43b85c4eb82e482f828d5e8e56f49f90dfac2d9bc3b4" +checksum = "7cef395fff17bf8f9c1dee6c0e12801a3ba24928139af0ecb5ccb82ff87bf9d2" dependencies = [ "proc-macro2", "quote", @@ -14389,6 +14765,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[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" @@ -14418,19 +14804,42 @@ dependencies = [ "windows-targets 0.52.6", ] +[[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", - "windows-interface", + "windows-implement 0.58.0", + "windows-interface 0.58.0", "windows-result 0.2.0", "windows-strings", "windows-targets 0.52.6", ] +[[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.87", +] + [[package]] name = "windows-implement" version = "0.58.0" @@ -14442,6 +14851,17 @@ dependencies = [ "syn 2.0.87", ] +[[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.87", +] + [[package]] name = "windows-interface" version = "0.58.0" @@ -14717,9 +15137,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] @@ -14814,7 +15234,7 @@ checksum = "d8a39a15d1ae2077688213611209849cad40e9e5cccf6e61951a425850677ff3" dependencies = [ "anyhow", "heck 0.4.1", - "indexmap 2.4.0", + "indexmap 2.6.0", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -14842,7 +15262,7 @@ checksum = "421c0c848a0660a8c22e2fd217929a0191f14476b68962afd2af89fd22e39825" dependencies = [ "anyhow", "bitflags 2.6.0", - "indexmap 2.4.0", + "indexmap 2.6.0", "log", "serde", "serde_derive", @@ -14861,7 +15281,7 @@ checksum = "196d3ecfc4b759a8573bf86a9b3f8996b304b3732e4c7de81655f875f6efdca6" dependencies = [ "anyhow", "id-arena", - "indexmap 2.4.0", + "indexmap 2.6.0", "log", "semver", "serde", @@ -14879,7 +15299,7 @@ checksum = "935a97eaffd57c3b413aa510f8f0b550a4a9fe7d59e79cd8b89a83dcb860321f" dependencies = [ "anyhow", "id-arena", - "indexmap 2.4.0", + "indexmap 2.6.0", "log", "semver", "serde", @@ -14897,7 +15317,7 @@ checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" dependencies = [ "anyhow", "log", - "thiserror", + "thiserror 1.0.69", "wast", ] @@ -14917,7 +15337,7 @@ dependencies = [ "derive_more", "env_logger 0.11.5", "fs", - "futures 0.3.30", + "futures 0.3.31", "git", "gpui", "http_client", @@ -14954,7 +15374,7 @@ dependencies = [ "collections", "env_logger 0.11.5", "fs", - "futures 0.3.30", + "futures 0.3.31", "fuzzy", "git", "git2", @@ -14981,6 +15401,18 @@ dependencies = [ "util", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "wyz" version = "0.5.1" @@ -14992,9 +15424,9 @@ dependencies = [ [[package]] name = "x11-clipboard" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98785a09322d7446e28a13203d2cae1059a0dd3dfb32cb06d0a225f023d8286" +checksum = "662d74b3d77e396b8e5beb00b9cad6a9eccf40b2ef68cc858784b14c41d535a3" dependencies = [ "libc", "x11rb", @@ -15009,7 +15441,7 @@ dependencies = [ "as-raw-xcb-connection", "gethostname", "libc", - "rustix 0.38.35", + "rustix 0.38.40", "x11rb-protocol", ] @@ -15157,6 +15589,30 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "synstructure", +] + [[package]] name = "zbus" version = "4.4.0" @@ -15166,9 +15622,9 @@ dependencies = [ "async-broadcast", "async-executor", "async-fs 2.1.2", - "async-io 2.3.4", + "async-io 2.4.0", "async-lock 3.4.0", - "async-process 2.2.4", + "async-process 2.3.0", "async-recursion 1.1.1", "async-task", "async-trait", @@ -15221,17 +15677,18 @@ dependencies = [ [[package]] name = "zed" -version = "0.163.0" +version = "0.164.0" dependencies = [ "activity_indicator", "anyhow", "ashpd", "assets", "assistant", - "assistant_slash_command", + "assistant2", "async-watch", "audio", "auto_update", + "auto_update_ui", "backtrace", "breadcrumbs", "call", @@ -15244,7 +15701,6 @@ dependencies = [ "collections", "command_palette", "command_palette_hooks", - "context_servers", "copilot", "db", "debugger_tools", @@ -15252,6 +15708,7 @@ dependencies = [ "diagnostics", "editor", "env_logger 0.11.5", + "extension", "extension_host", "extensions_ui", "feature_flags", @@ -15259,19 +15716,20 @@ dependencies = [ "file_finder", "file_icons", "fs", - "futures 0.3.30", + "futures 0.3.31", "git", "git_hosting_providers", "go_to_line", "gpui", "http_client", "image_viewer", - "indexed_docs", "inline_completion_button", "install_cli", "journal", "language", + "language_extension", "language_model", + "language_models", "language_selector", "language_tools", "languages", @@ -15287,12 +15745,12 @@ dependencies = [ "outline_panel", "parking_lot", "paths", + "picker", "profiling", "project", "project_panel", "project_symbols", "proto", - "quick_action_bar", "recent_projects", "release_channel", "remote", @@ -15318,6 +15776,7 @@ dependencies = [ "telemetry_events", "terminal_view", "theme", + "theme_extension", "theme_selector", "time", "toolchain_selector", @@ -15328,7 +15787,9 @@ dependencies = [ "urlencoding", "util", "uuid", + "vcs_menu", "vim", + "vim_mode_setting", "welcome", "windows 0.58.0", "winresource", @@ -15449,13 +15910,6 @@ dependencies = [ "zed_extension_api 0.1.0", ] -[[package]] -name = "zed_ocaml" -version = "0.1.0" -dependencies = [ - "zed_extension_api 0.1.0", -] - [[package]] name = "zed_php" version = "0.2.2" @@ -15561,6 +16015,27 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.1" @@ -15583,15 +16058,15 @@ dependencies = [ [[package]] name = "zeromq" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb0560d00172817b7f7c2265060783519c475702ae290b154115ca75e976d4d0" +checksum = "6a4528179201f6eecf211961a7d3276faa61554c82651ecc66387f68fc3004bd" dependencies = [ "async-dispatcher", "async-std", "async-trait", "asynchronous-codec", - "bytes 1.7.2", + "bytes 1.8.0", "crossbeam-queue", "dashmap 5.5.3", "futures-channel", @@ -15604,10 +16079,32 @@ dependencies = [ "parking_lot", "rand 0.8.5", "regex", - "thiserror", + "thiserror 1.0.69", "uuid", ] +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" diff --git a/Cargo.toml b/Cargo.toml index ad748ecb97..cc6a836238 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,10 +5,12 @@ members = [ "crates/anthropic", "crates/assets", "crates/assistant", + "crates/assistant2", "crates/assistant_slash_command", "crates/assistant_tool", "crates/audio", "crates/auto_update", + "crates/auto_update_ui", "crates/breadcrumbs", "crates/call", "crates/channel", @@ -52,11 +54,15 @@ members = [ "crates/http_client", "crates/image_viewer", "crates/indexed_docs", + "crates/inline_completion", "crates/inline_completion_button", "crates/install_cli", "crates/journal", "crates/language", + "crates/language_extension", "crates/language_model", + "crates/language_model_selector", + "crates/language_models", "crates/language_selector", "crates/language_tools", "crates/languages", @@ -81,7 +87,6 @@ members = [ "crates/project_panel", "crates/project_symbols", "crates/proto", - "crates/quick_action_bar", "crates/recent_projects", "crates/refineable", "crates/refineable/derive_refineable", @@ -117,6 +122,7 @@ members = [ "crates/terminal_view", "crates/text", "crates/theme", + "crates/theme_extension", "crates/theme_importer", "crates/theme_selector", "crates/time_format", @@ -129,6 +135,7 @@ members = [ "crates/util", "crates/vcs_menu", "crates/vim", + "crates/vim_mode_setting", "crates/welcome", "crates/workspace", "crates/worktree", @@ -151,7 +158,6 @@ members = [ "extensions/haskell", "extensions/html", "extensions/lua", - "extensions/ocaml", "extensions/php", "extensions/perplexity", "extensions/prisma", @@ -186,10 +192,12 @@ ai = { path = "crates/ai" } anthropic = { path = "crates/anthropic" } assets = { path = "crates/assets" } assistant = { path = "crates/assistant" } +assistant2 = { path = "crates/assistant2" } assistant_slash_command = { path = "crates/assistant_slash_command" } assistant_tool = { path = "crates/assistant_tool" } audio = { path = "crates/audio" } auto_update = { path = "crates/auto_update" } +auto_update_ui = { path = "crates/auto_update_ui" } breadcrumbs = { path = "crates/breadcrumbs" } call = { path = "crates/call" } channel = { path = "crates/channel" } @@ -230,11 +238,15 @@ html_to_markdown = { path = "crates/html_to_markdown" } http_client = { path = "crates/http_client" } image_viewer = { path = "crates/image_viewer" } indexed_docs = { path = "crates/indexed_docs" } +inline_completion = { path = "crates/inline_completion" } inline_completion_button = { path = "crates/inline_completion_button" } install_cli = { path = "crates/install_cli" } journal = { path = "crates/journal" } language = { path = "crates/language" } +language_extension = { path = "crates/language_extension" } language_model = { path = "crates/language_model" } +language_model_selector = { path = "crates/language_model_selector" } +language_models = { path = "crates/language_models" } language_selector = { path = "crates/language_selector" } language_tools = { path = "crates/language_tools" } languages = { path = "crates/languages" } @@ -261,7 +273,6 @@ project = { path = "crates/project" } project_panel = { path = "crates/project_panel" } project_symbols = { path = "crates/project_symbols" } proto = { path = "crates/proto" } -quick_action_bar = { path = "crates/quick_action_bar" } recent_projects = { path = "crates/recent_projects" } refineable = { path = "crates/refineable" } release_channel = { path = "crates/release_channel" } @@ -296,6 +307,7 @@ terminal = { path = "crates/terminal" } terminal_view = { path = "crates/terminal_view" } text = { path = "crates/text" } theme = { path = "crates/theme" } +theme_extension = { path = "crates/theme_extension" } theme_importer = { path = "crates/theme_importer" } theme_selector = { path = "crates/theme_selector" } time_format = { path = "crates/time_format" } @@ -307,6 +319,7 @@ ui_macros = { path = "crates/ui_macros" } util = { path = "crates/util" } vcs_menu = { path = "crates/vcs_menu" } vim = { path = "crates/vim" } +vim_mode_setting = { path = "crates/vim_mode_setting" } welcome = { path = "crates/welcome" } workspace = { path = "crates/workspace" } worktree = { path = "crates/worktree" } @@ -341,7 +354,7 @@ blade-macros = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a blade-util = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a13e642ad8401b1f3aa38e969" } blake3 = "1.5.3" bytes = "1.0" -cargo_metadata = "0.18" +cargo_metadata = "0.19" cargo_toml = "0.20" chrono = { version = "0.4", features = ["serde"] } clap = { version = "4.4", features = ["derive"] } @@ -377,12 +390,14 @@ indexmap = { version = "1.6.2", features = ["serde"] } indoc = "2" itertools = "0.13.0" jsonwebtoken = "9.3" +jupyter-protocol = { version = "0.3.0" } +jupyter-websocket-client = { version = "0.5.0" } libc = "0.2" linkify = "0.10.0" log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] } markup5ever_rcdom = "0.3.0" nanoid = "0.4" -nbformat = "0.5.0" +nbformat = { version = "0.7.0" } nix = "0.29" num-format = "0.4.4" once_cell = "1.19.0" @@ -397,7 +412,7 @@ pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" } pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" } postage = { version = "0.5", features = ["futures-traits"] } -pretty_assertions = "1.3.0" +pretty_assertions = { version = "1.3.0", features = ["unstable"] } profiling = "1" prost = "0.9" prost-build = "0.9" @@ -416,7 +431,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f "stream", ] } rsa = "0.9.6" -runtimelib = { version = "0.19.0", default-features = false, features = [ +runtimelib = { version = "0.22.0", default-features = false, features = [ "async-dispatcher-runtime", ] } rustc-demangle = "0.1.23" diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 664e13ee77..2eedc1c839 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -251,6 +251,8 @@ "ctrl-pagedown": "pane::ActivateNextItem", "ctrl-shift-pageup": "pane::SwapItemLeft", "ctrl-shift-pagedown": "pane::SwapItemRight", + "back": "pane::GoBack", + "forward": "pane::GoForward", "ctrl-w": "pane::CloseActiveItem", "ctrl-f4": "pane::CloseActiveItem", "alt-ctrl-t": ["pane::CloseInactiveItems", { "close_pinned": false }], @@ -647,11 +649,16 @@ "tab": "channel_modal::ToggleMode" } }, + { + "context": "FileFinder", + "bindings": { + "ctrl": "file_finder::ToggleMenu" + } + }, { "context": "FileFinder && !menu_open", "bindings": { "ctrl-shift-p": "file_finder::SelectPrev", - "ctrl": "file_finder::OpenMenu", "ctrl-j": "pane::SplitDown", "ctrl-k": "pane::SplitUp", "ctrl-h": "pane::SplitLeft", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 82edba3305..963d48ba5e 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -49,8 +49,9 @@ "ctrl-d": "editor::Delete", "tab": "editor::Tab", "shift-tab": "editor::TabPrev", - "ctrl-k": "editor::CutToEndOfLine", "ctrl-t": "editor::Transpose", + "ctrl-k": "editor::KillRingCut", + "ctrl-y": "editor::KillRingYank", "cmd-k q": "editor::Rewrap", "cmd-k cmd-q": "editor::Rewrap", "cmd-backspace": "editor::DeleteToBeginningOfLine", @@ -92,6 +93,8 @@ "ctrl-e": "editor::MoveToEndOfLine", "cmd-up": "editor::MoveToBeginning", "cmd-down": "editor::MoveToEnd", + "ctrl-home": "editor::MoveToBeginning", + "ctrl-end": "editor::MoveToEnd", "shift-up": "editor::SelectUp", "ctrl-shift-p": "editor::SelectUp", "shift-down": "editor::SelectDown", @@ -647,11 +650,16 @@ "tab": "channel_modal::ToggleMode" } }, + { + "context": "FileFinder", + "bindings": { + "cmd": "file_finder::ToggleMenu" + } + }, { "context": "FileFinder && !menu_open", "bindings": { "cmd-shift-p": "file_finder::SelectPrev", - "cmd": "file_finder::OpenMenu", "cmd-j": "pane::SplitDown", "cmd-k": "pane::SplitUp", "cmd-h": "pane::SplitLeft", diff --git a/assets/keymaps/linux/sublime_text.json b/assets/keymaps/linux/sublime_text.json index c4bffb56b0..57ef4b876b 100644 --- a/assets/keymaps/linux/sublime_text.json +++ b/assets/keymaps/linux/sublime_text.json @@ -16,6 +16,7 @@ "ctrl-shift-l": "editor::SplitSelectionIntoLines", "ctrl-shift-a": "editor::SelectLargerSyntaxNode", "ctrl-shift-d": "editor::DuplicateLineDown", + "alt-f3": "editor::SelectAllMatches", // find_all_under "f12": "editor::GoToDefinition", "ctrl-f12": "editor::GoToDefinitionSplit", "shift-f12": "editor::FindAllReferences", diff --git a/assets/keymaps/macos/sublime_text.json b/assets/keymaps/macos/sublime_text.json index dd57386424..f4c09b5144 100644 --- a/assets/keymaps/macos/sublime_text.json +++ b/assets/keymaps/macos/sublime_text.json @@ -19,6 +19,7 @@ "cmd-shift-l": "editor::SplitSelectionIntoLines", "cmd-shift-a": "editor::SelectLargerSyntaxNode", "cmd-shift-d": "editor::DuplicateLineDown", + "ctrl-cmd-g": "editor::SelectAllMatches", // find_all_under "shift-f12": "editor::FindAllReferences", "alt-cmd-down": "editor::GoToDefinition", "ctrl-alt-cmd-down": "editor::GoToDefinitionSplit", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 83e332a3f4..1be3e8c9c1 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -381,8 +381,7 @@ "shift-b": "vim::CurlyBrackets", "<": "vim::AngleBrackets", ">": "vim::AngleBrackets", - "a": "vim::AngleBrackets", - "g": "vim::Argument" + "a": "vim::Argument" } }, { @@ -578,7 +577,7 @@ } }, { - "context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView", + "context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || Welcome", "use_layout_keys": true, "bindings": { ":": "command_palette::Toggle", diff --git a/assets/settings/default.json b/assets/settings/default.json index e0952a258e..94484d250a 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -490,6 +490,9 @@ "version": "2", // Whether the assistant is enabled. "enabled": true, + // Whether to show inline hints showing the keybindings to use the inline assistant and the + // assistant panel. + "show_hints": false, // Whether to show the assistant panel button in the status bar. "button": true, // Where to dock the assistant panel. Can be 'left', 'right' or 'bottom'. @@ -580,7 +583,23 @@ // Settings related to the file finder. "file_finder": { // Whether to show file icons in the file finder. - "file_icons": true + "file_icons": true, + // Determines how much space the file finder can take up in relation to the available window width. + // There are 5 possible width values: + // + // 1. Small: This value is essentially a fixed width. + // "modal_width": "small" + // 2. Medium: + // "modal_width": "medium" + // 3. Large: + // "modal_width": "large" + // 4. Extra Large: + // "modal_width": "xlarge" + // 5. Fullscreen: This value removes any horizontal padding, as it consumes the whole viewport width. + // "modal_width": "full" + // + // Default: small + "modal_max_width": "small" }, // Whether or not to remove any trailing whitespace from lines of a buffer // before saving it. @@ -649,7 +668,7 @@ }, // Add files or globs of files that will be excluded by Zed entirely: // they will be skipped during FS scan(s), file tree and file search - // will lack the corresponding file entries. + // will lack the corresponding file entries. Overrides `file_scan_inclusions`. "file_scan_exclusions": [ "**/.git", "**/.svn", @@ -660,6 +679,11 @@ "**/.classpath", "**/.settings" ], + // Add files or globs of files that will be included by Zed, even when + // ignored by git. This is useful for files that are not tracked by git, + // but are still important to your project. Note that globs that are + // overly broad can slow down Zed's file scanning. Overridden by `file_scan_exclusions`. + "file_scan_inclusions": [".env*"], // Git gutter behavior configuration. "git": { // Control whether the git gutter is shown. May take 2 values: @@ -820,8 +844,12 @@ } }, "toolbar": { - // Whether to display the terminal title in its toolbar. - "title": true + // Whether to display the terminal title in its toolbar's breadcrumbs. + // Only shown if the terminal title is not empty. + // + // The shell running in the terminal needs to be configured to emit the title. + // Example: `echo -e "\e]2;New Title\007";` + "breadcrumbs": true } // Set the terminal's font size. If this option is not included, // the terminal will default to matching the buffer's font size. @@ -857,15 +885,8 @@ // "file_types": { "Plain Text": ["txt"], - "JSON": ["flake.lock"], - "JSONC": [ - "**/.zed/**/*.json", - "**/zed/**/*.json", - "**/Zed/**/*.json", - "tsconfig.json", - "pyrightconfig.json" - ], - "TOML": ["uv.lock"] + "JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json"], + "Shell Script": [".env.*"] }, /// By default use a recent system version of node, or install our own. /// You can override this to use a version of node that is not in $PATH with: diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index 21153b6fcc..0799d4bbdb 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -50,6 +50,8 @@ indexed_docs.workspace = true indoc.workspace = true language.workspace = true language_model.workspace = true +language_model_selector.workspace = true +language_models.workspace = true log.workspace = true lsp.workspace = true markdown.workspace = true diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 88500247c3..f6e435bfb8 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -5,7 +5,6 @@ pub mod assistant_settings; mod context; pub mod context_store; mod inline_assistant; -mod model_selector; mod patch; mod prompt_library; mod prompts; @@ -33,12 +32,10 @@ use feature_flags::FeatureFlagAppExt; use fs::Fs; use gpui::impl_actions; use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal}; -use indexed_docs::IndexedDocsRegistry; pub(crate) use inline_assistant::*; use language_model::{ LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage, }; -pub(crate) use model_selector::*; pub use patch::*; pub use prompts::PromptBuilder; use prompts::PromptLoadingParams; @@ -275,7 +272,7 @@ pub fn init( client.telemetry().clone(), cx, ); - IndexedDocsRegistry::init_global(cx); + indexed_docs::init(cx); CommandPaletteFilter::update_global(cx, |filter, _cx| { filter.hide_namespace(Assistant::NAMESPACE); diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 76450c983b..ce8a71c7d4 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -17,9 +17,9 @@ use crate::{ ContextEvent, ContextId, ContextStore, ContextStoreEvent, CopyCode, CycleMessageRole, DeployHistory, DeployPromptLibrary, Edit, InlineAssistant, InsertDraggedFiles, InsertIntoEditor, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId, - MessageMetadata, MessageStatus, ModelPickerDelegate, ModelSelector, NewContext, - ParsedSlashCommand, PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata, - RequestType, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector, + MessageMetadata, MessageStatus, NewContext, ParsedSlashCommand, PendingSlashCommandStatus, + QuoteSelection, RemoteContextMetadata, RequestType, SavedContextMetadata, Split, ToggleFocus, + ToggleModelSelector, }; use anyhow::Result; use assistant_slash_command::{SlashCommand, SlashCommandOutputSection}; @@ -50,11 +50,12 @@ use indexed_docs::IndexedDocsStore; use language::{ language_settings::SoftWrap, BufferSnapshot, LanguageRegistry, LspAdapterDelegate, ToOffset, }; -use language_model::{ - provider::cloud::PROVIDER_ID, LanguageModelProvider, LanguageModelProviderId, - LanguageModelRegistry, Role, -}; use language_model::{LanguageModelImage, LanguageModelToolUse}; +use language_model::{ + LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role, + ZED_CLOUD_PROVIDER_ID, +}; +use language_model_selector::{LanguageModelPickerDelegate, LanguageModelSelector}; use multi_buffer::MultiBufferRow; use picker::{Picker, PickerDelegate}; use project::lsp_store::LocalLspAdapterDelegate; @@ -142,7 +143,7 @@ pub struct AssistantPanel { languages: Arc, fs: Arc, subscriptions: Vec, - model_selector_menu_handle: PopoverMenuHandle>, + model_selector_menu_handle: PopoverMenuHandle>, model_summary_editor: View, authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>, configuration_subscription: Option, @@ -664,7 +665,7 @@ impl AssistantPanel { // If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is // the provider, we want to show a nudge to sign in. let show_zed_ai_notice = client_status.is_signed_out() - && active_provider.map_or(true, |provider| provider.id().0 == PROVIDER_ID); + && active_provider.map_or(true, |provider| provider.id().0 == ZED_CLOUD_PROVIDER_ID); self.show_zed_ai_notice = show_zed_ai_notice; cx.notify(); @@ -2050,30 +2051,6 @@ impl ContextEditor { ContextEvent::SlashCommandOutputSectionAdded { section } => { self.insert_slash_command_output_sections([section.clone()], false, cx); } - ContextEvent::SlashCommandFinished { - output_range: _output_range, - run_commands_in_ranges, - } => { - for range in run_commands_in_ranges { - let commands = self.context.update(cx, |context, cx| { - context.reparse(cx); - context - .pending_commands_for_range(range.clone(), cx) - .to_vec() - }); - - for command in commands { - self.run_command( - command.source_range, - &command.name, - &command.arguments, - false, - self.workspace.clone(), - cx, - ); - } - } - } ContextEvent::UsePendingTools => { let pending_tool_uses = self .context @@ -2152,6 +2129,37 @@ impl ContextEditor { command_id: InvokedSlashCommandId, cx: &mut ViewContext, ) { + if let Some(invoked_slash_command) = + self.context.read(cx).invoked_slash_command(&command_id) + { + if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status { + let run_commands_in_ranges = invoked_slash_command + .run_commands_in_ranges + .iter() + .cloned() + .collect::>(); + for range in run_commands_in_ranges { + let commands = self.context.update(cx, |context, cx| { + context.reparse(cx); + context + .pending_commands_for_range(range.clone(), cx) + .to_vec() + }); + + for command in commands { + self.run_command( + command.source_range, + &command.name, + &command.arguments, + false, + self.workspace.clone(), + cx, + ); + } + } + } + } + self.editor.update(cx, |editor, cx| { if let Some(invoked_slash_command) = self.context.read(cx).invoked_slash_command(&command_id) @@ -3333,7 +3341,8 @@ impl ContextEditor { self.context.update(cx, |context, cx| { for image in images { - let Some(render_image) = image.to_image_data(cx).log_err() else { + let Some(render_image) = image.to_image_data(cx.svg_renderer()).log_err() + else { continue; }; let image_id = image.id(); @@ -4449,13 +4458,13 @@ pub struct ContextEditorToolbarItem { fs: Arc, active_context_editor: Option>, model_summary_editor: View, - model_selector_menu_handle: PopoverMenuHandle>, + model_selector_menu_handle: PopoverMenuHandle>, } impl ContextEditorToolbarItem { pub fn new( workspace: &Workspace, - model_selector_menu_handle: PopoverMenuHandle>, + model_selector_menu_handle: PopoverMenuHandle>, model_summary_editor: View, ) -> Self { Self { @@ -4551,8 +4560,17 @@ impl Render for ContextEditorToolbarItem { // .map(|remaining_items| format!("Files to scan: {}", remaining_items)) // }) .child( - ModelSelector::new( - self.fs.clone(), + LanguageModelSelector::new( + { + let fs = self.fs.clone(); + move |model, cx| { + update_settings_file::( + fs.clone(), + cx, + move |settings, _| settings.set_model(model.clone()), + ); + } + }, ButtonLike::new("active-model") .style(ButtonStyle::Subtle) .child( diff --git a/crates/assistant/src/assistant_settings.rs b/crates/assistant/src/assistant_settings.rs index 5bfd406658..a782f05d03 100644 --- a/crates/assistant/src/assistant_settings.rs +++ b/crates/assistant/src/assistant_settings.rs @@ -5,13 +5,12 @@ use anthropic::Model as AnthropicModel; use feature_flags::FeatureFlagAppExt; use fs::Fs; use gpui::{AppContext, Pixels}; -use language_model::provider::open_ai; -use language_model::settings::{ - AnthropicSettingsContent, AnthropicSettingsContentV1, OllamaSettingsContent, - OpenAiSettingsContent, OpenAiSettingsContentV1, VersionedAnthropicSettingsContent, - VersionedOpenAiSettingsContent, +use language_model::{CloudModel, LanguageModel}; +use language_models::{ + provider::open_ai, AllLanguageModelSettings, AnthropicSettingsContent, + AnthropicSettingsContentV1, OllamaSettingsContent, OpenAiSettingsContent, + OpenAiSettingsContentV1, VersionedAnthropicSettingsContent, VersionedOpenAiSettingsContent, }; -use language_model::{settings::AllLanguageModelSettings, CloudModel, LanguageModel}; use ollama::Model as OllamaModel; use schemars::{schema::Schema, JsonSchema}; use serde::{Deserialize, Serialize}; @@ -60,6 +59,7 @@ pub struct AssistantSettings { pub inline_alternatives: Vec, pub using_outdated_settings_version: bool, pub enable_experimental_live_diffs: bool, + pub show_hints: bool, } impl AssistantSettings { @@ -202,6 +202,7 @@ impl AssistantSettingsContent { AssistantSettingsContent::Versioned(settings) => match settings { VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 { enabled: settings.enabled, + show_hints: None, button: settings.button, dock: settings.dock, default_width: settings.default_width, @@ -242,6 +243,7 @@ impl AssistantSettingsContent { }, AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 { enabled: None, + show_hints: None, button: settings.button, dock: settings.dock, default_width: settings.default_width, @@ -354,6 +356,7 @@ impl Default for VersionedAssistantSettingsContent { fn default() -> Self { Self::V2(AssistantSettingsContentV2 { enabled: None, + show_hints: None, button: None, dock: None, default_width: None, @@ -371,6 +374,11 @@ pub struct AssistantSettingsContentV2 { /// /// Default: true enabled: Option, + /// Whether to show inline hints that show keybindings for inline assistant + /// and assistant panel. + /// + /// Default: true + show_hints: Option, /// Whether to show the assistant panel button in the status bar. /// /// Default: true @@ -505,6 +513,7 @@ impl Settings for AssistantSettings { let value = value.upgrade(); merge(&mut settings.enabled, value.enabled); + merge(&mut settings.show_hints, value.show_hints); merge(&mut settings.button, value.button); merge(&mut settings.dock, value.dock); merge( @@ -575,6 +584,7 @@ mod tests { }), inline_alternatives: None, enabled: None, + show_hints: None, button: None, dock: None, default_width: None, diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 807d03ea5f..570180ed74 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -25,13 +25,15 @@ use gpui::{ use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset}; use language_model::{ - logging::report_assistant_event, - provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError}, LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent, LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role, StopReason, }; +use language_models::{ + provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError}, + report_assistant_event, +}; use open_ai::Model as OpenAiModel; use paths::contexts_dir; use project::Project; @@ -381,10 +383,6 @@ pub enum ContextEvent { SlashCommandOutputSectionAdded { section: SlashCommandOutputSection, }, - SlashCommandFinished { - output_range: Range, - run_commands_in_ranges: Vec>, - }, UsePendingTools, ToolFinished { tool_use_id: Arc, @@ -916,6 +914,7 @@ impl Context { InvokedSlashCommand { name: name.into(), range: output_range, + run_commands_in_ranges: Vec::new(), status: InvokedSlashCommandStatus::Running(Task::ready(())), transaction: None, timestamp: id.0, @@ -1914,7 +1913,6 @@ impl Context { } let mut pending_section_stack: Vec = Vec::new(); - let mut run_commands_in_ranges: Vec> = Vec::new(); let mut last_role: Option = None; let mut last_section_range = None; @@ -1980,7 +1978,13 @@ impl Context { let end = this.buffer.read(cx).anchor_before(insert_position); if run_commands_in_text { - run_commands_in_ranges.push(start..end); + if let Some(invoked_slash_command) = + this.invoked_slash_commands.get_mut(&command_id) + { + invoked_slash_command + .run_commands_in_ranges + .push(start..end); + } } } SlashCommandEvent::EndSection => { @@ -2100,6 +2104,7 @@ impl Context { InvokedSlashCommand { name: name.to_string().into(), range: command_range.clone(), + run_commands_in_ranges: Vec::new(), status: InvokedSlashCommandStatus::Running(insert_output_task), transaction: Some(first_transaction), timestamp: command_id.0, @@ -2891,7 +2896,7 @@ impl Context { request.messages.push(LanguageModelRequestMessage { role: Role::User, content: vec![ - "Generate a concise 3-7 word title for this conversation, omitting punctuation" + "Generate a concise 3-7 word title for this conversation, omitting punctuation. Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`" .into(), ], cache: false, @@ -3176,6 +3181,7 @@ pub struct ParsedSlashCommand { pub struct InvokedSlashCommand { pub name: SharedString, pub range: Range, + pub run_commands_in_ranges: Vec>, pub status: InvokedSlashCommandStatus, pub transaction: Option, timestamp: clock::Lamport, diff --git a/crates/assistant/src/context_store.rs b/crates/assistant/src/context_store.rs index 568b04e492..217d59faa4 100644 --- a/crates/assistant/src/context_store.rs +++ b/crates/assistant/src/context_store.rs @@ -770,7 +770,7 @@ impl ContextStore { contexts.push(SavedContextMetadata { title: title.to_string(), path, - mtime: metadata.mtime.into(), + mtime: metadata.mtime.timestamp_for_user().into(), }); } } diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 22620ca2c2..b1cb1d81b4 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -1,7 +1,7 @@ use crate::{ assistant_settings::AssistantSettings, humanize_token_count, prompts::PromptBuilder, AssistantPanel, AssistantPanelEvent, CharOperation, CycleNextInlineAssist, - CyclePreviousInlineAssist, LineDiff, LineOperation, ModelSelector, RequestType, StreamingDiff, + CyclePreviousInlineAssist, LineDiff, LineOperation, RequestType, StreamingDiff, }; use anyhow::{anyhow, Context as _, Result}; use client::{telemetry::Telemetry, ErrorExt}; @@ -30,14 +30,16 @@ use gpui::{ }; use language::{Buffer, IndentKind, Point, Selection, TransactionId}; use language_model::{ - logging::report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest, - LanguageModelRequestMessage, LanguageModelTextStream, Role, + LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, + LanguageModelTextStream, Role, }; +use language_model_selector::LanguageModelSelector; +use language_models::report_assistant_event; use multi_buffer::MultiBufferRow; use parking_lot::Mutex; use project::{CodeAction, ProjectTransaction}; use rope::Rope; -use settings::{Settings, SettingsStore}; +use settings::{update_settings_file, Settings, SettingsStore}; use smol::future::FutureExt; use std::{ cmp, @@ -1499,8 +1501,17 @@ impl Render for PromptEditor { .justify_center() .gap_2() .child( - ModelSelector::new( - self.fs.clone(), + LanguageModelSelector::new( + { + let fs = self.fs.clone(); + move |model, cx| { + update_settings_file::( + fs.clone(), + cx, + move |settings, _| settings.set_model(model.clone()), + ); + } + }, IconButton::new("context", IconName::SettingsAlt) .shape(IconButtonShape::Square) .icon_size(IconSize::Small) @@ -1520,7 +1531,7 @@ impl Render for PromptEditor { ) }), ) - .with_info_text( + .info_text( "Inline edits use context\n\ from the currently selected\n\ assistant panel tab.", diff --git a/crates/assistant/src/slash_command/default_command.rs b/crates/assistant/src/slash_command/default_command.rs index 4d9c9e2ae4..49a7b244e9 100644 --- a/crates/assistant/src/slash_command/default_command.rs +++ b/crates/assistant/src/slash_command/default_command.rs @@ -69,6 +69,10 @@ impl SlashCommand for DefaultSlashCommand { text.push('\n'); } + if !text.ends_with('\n') { + text.push('\n'); + } + Ok(SlashCommandOutput { sections: vec![SlashCommandOutputSection { range: 0..text.len(), diff --git a/crates/assistant/src/terminal_inline_assistant.rs b/crates/assistant/src/terminal_inline_assistant.rs index 2fb4b4ffda..a5424a8d7e 100644 --- a/crates/assistant/src/terminal_inline_assistant.rs +++ b/crates/assistant/src/terminal_inline_assistant.rs @@ -1,6 +1,7 @@ +use crate::assistant_settings::AssistantSettings; use crate::{ - humanize_token_count, prompts::PromptBuilder, AssistantPanel, AssistantPanelEvent, - ModelSelector, RequestType, DEFAULT_CONTEXT_LINES, + humanize_token_count, prompts::PromptBuilder, AssistantPanel, AssistantPanelEvent, RequestType, + DEFAULT_CONTEXT_LINES, }; use anyhow::{Context as _, Result}; use client::telemetry::Telemetry; @@ -17,10 +18,11 @@ use gpui::{ }; use language::Buffer; use language_model::{ - logging::report_assistant_event, LanguageModelRegistry, LanguageModelRequest, - LanguageModelRequestMessage, Role, + LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role, }; -use settings::Settings; +use language_model_selector::LanguageModelSelector; +use language_models::report_assistant_event; +use settings::{update_settings_file, Settings}; use std::{ cmp, sync::Arc, @@ -612,8 +614,17 @@ impl Render for PromptEditor { .w_12() .justify_center() .gap_2() - .child(ModelSelector::new( - self.fs.clone(), + .child(LanguageModelSelector::new( + { + let fs = self.fs.clone(); + move |model, cx| { + update_settings_file::( + fs.clone(), + cx, + move |settings, _| settings.set_model(model.clone()), + ); + } + }, IconButton::new("context", IconName::SettingsAlt) .shape(IconButtonShape::Square) .icon_size(IconSize::Small) diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml new file mode 100644 index 0000000000..9dd605d559 --- /dev/null +++ b/crates/assistant2/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "assistant2" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/assistant.rs" +doctest = false + +[dependencies] +anyhow.workspace = true +command_palette_hooks.workspace = true +editor.workspace = true +feature_flags.workspace = true +gpui.workspace = true +language_model.workspace = true +language_model_selector.workspace = true +proto.workspace = true +settings.workspace = true +theme.workspace = true +ui.workspace = true +workspace.workspace = true diff --git a/crates/quick_action_bar/LICENSE-GPL b/crates/assistant2/LICENSE-GPL similarity index 100% rename from crates/quick_action_bar/LICENSE-GPL rename to crates/assistant2/LICENSE-GPL diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs new file mode 100644 index 0000000000..6a80186525 --- /dev/null +++ b/crates/assistant2/src/assistant.rs @@ -0,0 +1,41 @@ +mod assistant_panel; +mod message_editor; + +use command_palette_hooks::CommandPaletteFilter; +use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt}; +use gpui::{actions, AppContext}; + +pub use crate::assistant_panel::AssistantPanel; + +actions!(assistant2, [ToggleFocus, NewThread, ToggleModelSelector]); + +const NAMESPACE: &str = "assistant2"; + +/// Initializes the `assistant2` crate. +pub fn init(cx: &mut AppContext) { + assistant_panel::init(cx); + feature_gate_assistant2_actions(cx); +} + +fn feature_gate_assistant2_actions(cx: &mut AppContext) { + const ASSISTANT1_NAMESPACE: &str = "assistant"; + + CommandPaletteFilter::update_global(cx, |filter, _cx| { + filter.hide_namespace(NAMESPACE); + }); + + cx.observe_flag::(move |is_enabled, cx| { + if is_enabled { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + filter.show_namespace(NAMESPACE); + filter.hide_namespace(ASSISTANT1_NAMESPACE); + }); + } else { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + filter.hide_namespace(NAMESPACE); + filter.show_namespace(ASSISTANT1_NAMESPACE); + }); + } + }) + .detach(); +} diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs new file mode 100644 index 0000000000..d38f696e9a --- /dev/null +++ b/crates/assistant2/src/assistant_panel.rs @@ -0,0 +1,258 @@ +use anyhow::Result; +use gpui::{ + prelude::*, px, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, + FocusableView, Pixels, Task, View, ViewContext, WeakView, WindowContext, +}; +use language_model::LanguageModelRegistry; +use language_model_selector::LanguageModelSelector; +use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, Tab, Tooltip}; +use workspace::dock::{DockPosition, Panel, PanelEvent}; +use workspace::{Pane, Workspace}; + +use crate::message_editor::MessageEditor; +use crate::{NewThread, ToggleFocus, ToggleModelSelector}; + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views( + |workspace: &mut Workspace, _cx: &mut ViewContext| { + workspace.register_action(|workspace, _: &ToggleFocus, cx| { + workspace.toggle_panel_focus::(cx); + }); + }, + ) + .detach(); +} + +pub struct AssistantPanel { + pane: View, + message_editor: View, +} + +impl AssistantPanel { + pub fn load( + workspace: WeakView, + cx: AsyncWindowContext, + ) -> Task>> { + cx.spawn(|mut cx| async move { + workspace.update(&mut cx, |workspace, cx| { + cx.new_view(|cx| Self::new(workspace, cx)) + }) + }) + } + + fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { + let pane = cx.new_view(|cx| { + let mut pane = Pane::new( + workspace.weak_handle(), + workspace.project().clone(), + Default::default(), + None, + Some(NewThread.boxed_clone()), + cx, + ); + pane.set_can_split(false, cx); + pane.set_can_navigate(true, cx); + + pane + }); + + Self { + pane, + message_editor: cx.new_view(MessageEditor::new), + } + } +} + +impl FocusableView for AssistantPanel { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.pane.focus_handle(cx) + } +} + +impl EventEmitter for AssistantPanel {} + +impl Panel for AssistantPanel { + fn persistent_name() -> &'static str { + "AssistantPanel2" + } + + fn position(&self, _cx: &WindowContext) -> DockPosition { + DockPosition::Right + } + + fn position_is_valid(&self, _: DockPosition) -> bool { + true + } + + fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext) {} + + fn size(&self, _cx: &WindowContext) -> Pixels { + px(640.) + } + + fn set_size(&mut self, _size: Option, _cx: &mut ViewContext) {} + + fn is_zoomed(&self, cx: &WindowContext) -> bool { + self.pane.read(cx).is_zoomed() + } + + fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { + self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx)); + } + + fn set_active(&mut self, _active: bool, _cx: &mut ViewContext) {} + + fn pane(&self) -> Option> { + Some(self.pane.clone()) + } + + fn remote_id() -> Option { + Some(proto::PanelId::AssistantPanel) + } + + fn icon(&self, _cx: &WindowContext) -> Option { + Some(IconName::ZedAssistant) + } + + fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { + Some("Assistant Panel") + } + + fn toggle_action(&self) -> Box { + Box::new(ToggleFocus) + } +} + +impl AssistantPanel { + fn render_toolbar(&self, cx: &mut ViewContext) -> impl IntoElement { + let focus_handle = self.focus_handle(cx); + + h_flex() + .id("assistant-toolbar") + .justify_between() + .gap(DynamicSpacing::Base08.rems(cx)) + .h(Tab::container_height(cx)) + .px(DynamicSpacing::Base08.rems(cx)) + .bg(cx.theme().colors().tab_bar_background) + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .child(h_flex().child(Label::new("Thread Title Goes Here"))) + .child( + h_flex() + .gap(DynamicSpacing::Base08.rems(cx)) + .child(self.render_language_model_selector(cx)) + .child(Divider::vertical()) + .child( + IconButton::new("new-thread", IconName::Plus) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .style(ButtonStyle::Subtle) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |cx| { + Tooltip::for_action_in( + "New Thread", + &NewThread, + &focus_handle, + cx, + ) + } + }) + .on_click(move |_event, _cx| { + println!("New Thread"); + }), + ) + .child( + IconButton::new("open-history", IconName::HistoryRerun) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text("Open History", cx)) + .on_click(move |_event, _cx| { + println!("Open History"); + }), + ) + .child( + IconButton::new("configure-assistant", IconName::Settings) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text("Configure Assistant", cx)) + .on_click(move |_event, _cx| { + println!("Configure Assistant"); + }), + ), + ) + } + + fn render_language_model_selector(&self, cx: &mut ViewContext) -> impl IntoElement { + let active_provider = LanguageModelRegistry::read_global(cx).active_provider(); + let active_model = LanguageModelRegistry::read_global(cx).active_model(); + + LanguageModelSelector::new( + |model, _cx| { + println!("Selected {:?}", model.name()); + }, + ButtonLike::new("active-model") + .style(ButtonStyle::Subtle) + .child( + h_flex() + .w_full() + .gap_0p5() + .child( + div() + .overflow_x_hidden() + .flex_grow() + .whitespace_nowrap() + .child(match (active_provider, active_model) { + (Some(provider), Some(model)) => h_flex() + .gap_1() + .child( + Icon::new( + model.icon().unwrap_or_else(|| provider.icon()), + ) + .color(Color::Muted) + .size(IconSize::XSmall), + ) + .child( + Label::new(model.name().0) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .into_any_element(), + _ => Label::new("No model selected") + .size(LabelSize::Small) + .color(Color::Muted) + .into_any_element(), + }), + ) + .child( + Icon::new(IconName::ChevronDown) + .color(Color::Muted) + .size(IconSize::XSmall), + ), + ) + .tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)), + ) + } +} + +impl Render for AssistantPanel { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + v_flex() + .key_context("AssistantPanel2") + .justify_between() + .size_full() + .on_action(cx.listener(|_this, _: &NewThread, _cx| { + println!("Action: New Thread"); + })) + .child(self.render_toolbar(cx)) + .child(v_flex().bg(cx.theme().colors().panel_background)) + .child( + h_flex() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + .child(self.message_editor.clone()), + ) + } +} diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs new file mode 100644 index 0000000000..ee25ad5da7 --- /dev/null +++ b/crates/assistant2/src/message_editor.rs @@ -0,0 +1,76 @@ +use editor::{Editor, EditorElement, EditorStyle}; +use gpui::{TextStyle, View}; +use settings::Settings; +use theme::ThemeSettings; +use ui::prelude::*; + +pub struct MessageEditor { + editor: View, +} + +impl MessageEditor { + pub fn new(cx: &mut ViewContext) -> Self { + Self { + editor: cx.new_view(|cx| { + let mut editor = Editor::auto_height(80, cx); + editor.set_placeholder_text("Ask anything…", cx); + + editor + }), + } + } +} + +impl Render for MessageEditor { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let font_size = TextSize::Default.rems(cx); + let line_height = font_size.to_pixels(cx.rem_size()) * 1.3; + + v_flex() + .size_full() + .gap_2() + .p_2() + .bg(cx.theme().colors().editor_background) + .child({ + let settings = ThemeSettings::get_global(cx); + let text_style = TextStyle { + color: cx.theme().colors().editor_foreground, + font_family: settings.ui_font.family.clone(), + font_features: settings.ui_font.features.clone(), + font_size: font_size.into(), + font_weight: settings.ui_font.weight, + line_height: line_height.into(), + ..Default::default() + }; + + EditorElement::new( + &self.editor, + EditorStyle { + background: cx.theme().colors().editor_background, + local_player: cx.theme().players().local(), + text: text_style, + ..Default::default() + }, + ) + }) + .child( + h_flex() + .justify_between() + .child( + h_flex().child( + Button::new("add-context", "Add Context") + .style(ButtonStyle::Filled) + .icon(IconName::Plus) + .icon_position(IconPosition::Start), + ), + ) + .child( + h_flex() + .gap_2() + .child(Button::new("codebase", "Codebase").style(ButtonStyle::Filled)) + .child(Label::new("or")) + .child(Button::new("chat", "Chat").style(ButtonStyle::Filled)), + ), + ) + } +} diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index 3fb2dc66b2..59d98ee770 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -18,6 +18,7 @@ use workspace::{ui::IconName, Workspace}; pub fn init(cx: &mut AppContext) { SlashCommandRegistry::default_global(cx); + extension_slash_command::init(cx); } #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/crates/assistant_slash_command/src/extension_slash_command.rs b/crates/assistant_slash_command/src/extension_slash_command.rs index bfb2688066..2279f93b1c 100644 --- a/crates/assistant_slash_command/src/extension_slash_command.rs +++ b/crates/assistant_slash_command/src/extension_slash_command.rs @@ -3,17 +3,39 @@ use std::sync::{atomic::AtomicBool, Arc}; use anyhow::Result; use async_trait::async_trait; -use extension::{Extension, WorktreeDelegate}; -use gpui::{Task, WeakView, WindowContext}; +use extension::{Extension, ExtensionHostProxy, ExtensionSlashCommandProxy, WorktreeDelegate}; +use gpui::{AppContext, Task, WeakView, WindowContext}; use language::{BufferSnapshot, LspAdapterDelegate}; use ui::prelude::*; use workspace::Workspace; use crate::{ ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, - SlashCommandResult, + SlashCommandRegistry, SlashCommandResult, }; +pub fn init(cx: &mut AppContext) { + let proxy = ExtensionHostProxy::default_global(cx); + proxy.register_slash_command_proxy(SlashCommandRegistryProxy { + slash_command_registry: SlashCommandRegistry::global(cx), + }); +} + +struct SlashCommandRegistryProxy { + slash_command_registry: Arc, +} + +impl ExtensionSlashCommandProxy for SlashCommandRegistryProxy { + fn register_slash_command( + &self, + extension: Arc, + command: extension::SlashCommand, + ) { + self.slash_command_registry + .register_command(ExtensionSlashCommand::new(extension, command), false) + } +} + /// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`]. struct WorktreeDelegateAdapter(Arc); diff --git a/crates/auto_update/Cargo.toml b/crates/auto_update/Cargo.toml index d47a9f9ae0..fa46b04a78 100644 --- a/crates/auto_update/Cargo.toml +++ b/crates/auto_update/Cargo.toml @@ -16,21 +16,16 @@ doctest = false anyhow.workspace = true client.workspace = true db.workspace = true -editor.workspace = true gpui.workspace = true http_client.workspace = true log.workspace = true -markdown_preview.workspace = true -menu.workspace = true paths.workspace = true release_channel.workspace = true schemars.workspace = true serde.workspace = true -serde_derive.workspace = true serde_json.workspace = true settings.workspace = true smol.workspace = true tempfile.workspace = true -util.workspace = true which.workspace = true workspace.workspace = true diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 6d95daecb7..0f9999b918 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -1,27 +1,19 @@ -mod update_notification; - use anyhow::{anyhow, Context, Result}; use client::{Client, TelemetrySettings}; use db::kvp::KEY_VALUE_STORE; use db::RELEASE_CHANNEL; -use editor::{Editor, MultiBuffer}; use gpui::{ actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext, - SemanticVersion, SharedString, Task, View, ViewContext, VisualContext, WindowContext, + SemanticVersion, Task, WindowContext, }; - -use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView}; -use paths::remote_servers_dir; -use schemars::JsonSchema; -use serde::Deserialize; -use serde_derive::Serialize; -use smol::{fs, io::AsyncReadExt}; - -use settings::{Settings, SettingsSources, SettingsStore}; -use smol::{fs::File, process::Command}; - use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; -use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; +use paths::remote_servers_dir; +use release_channel::{AppCommitSha, ReleaseChannel}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::{Settings, SettingsSources, SettingsStore}; +use smol::{fs, io::AsyncReadExt}; +use smol::{fs::File, process::Command}; use std::{ env::{ self, @@ -32,24 +24,13 @@ use std::{ sync::Arc, time::Duration, }; -use update_notification::UpdateNotification; -use util::ResultExt; use which::which; -use workspace::notifications::NotificationId; use workspace::Workspace; const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification"; const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60); -actions!( - auto_update, - [ - Check, - DismissErrorMessage, - ViewReleaseNotes, - ViewReleaseNotesLocally - ] -); +actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes,]); #[derive(Serialize)] struct UpdateRequestBody { @@ -146,12 +127,6 @@ struct GlobalAutoUpdate(Option>); impl Global for GlobalAutoUpdate {} -#[derive(Deserialize)] -struct ReleaseNotesBody { - title: String, - release_notes: String, -} - pub fn init(http_client: Arc, cx: &mut AppContext) { AutoUpdateSetting::register(cx); @@ -161,10 +136,6 @@ pub fn init(http_client: Arc, cx: &mut AppContext) { workspace.register_action(|_, action, cx| { view_release_notes(action, cx); }); - - workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| { - view_release_notes_locally(workspace, cx); - }); }) .detach(); @@ -264,121 +235,6 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<( None } -fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext) { - let release_channel = ReleaseChannel::global(cx); - - let url = match release_channel { - ReleaseChannel::Nightly => Some("https://github.com/zed-industries/zed/commits/nightly/"), - ReleaseChannel::Dev => Some("https://github.com/zed-industries/zed/commits/main/"), - _ => None, - }; - - if let Some(url) = url { - cx.open_url(url); - return; - } - - let version = AppVersion::global(cx).to_string(); - - let client = client::Client::global(cx).http_client(); - let url = client.build_url(&format!( - "/api/release_notes/v2/{}/{}", - release_channel.dev_name(), - version - )); - - let markdown = workspace - .app_state() - .languages - .language_for_name("Markdown"); - - workspace - .with_local_workspace(cx, move |_, cx| { - cx.spawn(|workspace, mut cx| async move { - let markdown = markdown.await.log_err(); - let response = client.get(&url, Default::default(), true).await; - let Some(mut response) = response.log_err() else { - return; - }; - - let mut body = Vec::new(); - response.body_mut().read_to_end(&mut body).await.ok(); - - let body: serde_json::Result = - serde_json::from_slice(body.as_slice()); - - if let Ok(body) = body { - workspace - .update(&mut cx, |workspace, cx| { - let project = workspace.project().clone(); - let buffer = project.update(cx, |project, cx| { - project.create_local_buffer("", markdown, cx) - }); - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, body.release_notes)], None, cx) - }); - let language_registry = project.read(cx).languages().clone(); - - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - - let tab_description = SharedString::from(body.title.to_string()); - let editor = cx.new_view(|cx| { - Editor::for_multibuffer(buffer, Some(project), true, cx) - }); - let workspace_handle = workspace.weak_handle(); - let view: View = MarkdownPreviewView::new( - MarkdownPreviewMode::Default, - editor, - workspace_handle, - language_registry, - Some(tab_description), - cx, - ); - workspace.add_item_to_active_pane( - Box::new(view.clone()), - None, - true, - cx, - ); - cx.notify(); - }) - .log_err(); - } - }) - .detach(); - }) - .detach(); -} - -pub fn notify_of_any_new_update(cx: &mut ViewContext) -> Option<()> { - let updater = AutoUpdater::get(cx)?; - let version = updater.read(cx).current_version; - let should_show_notification = updater.read(cx).should_show_update_notification(cx); - - cx.spawn(|workspace, mut cx| async move { - let should_show_notification = should_show_notification.await?; - if should_show_notification { - workspace.update(&mut cx, |workspace, cx| { - let workspace_handle = workspace.weak_handle(); - workspace.show_notification( - NotificationId::unique::(), - cx, - |cx| cx.new_view(|_| UpdateNotification::new(version, workspace_handle)), - ); - updater.update(cx, |updater, cx| { - updater - .set_should_show_update_notification(false, cx) - .detach_and_log_err(cx); - }); - })?; - } - anyhow::Ok(()) - }) - .detach(); - - None -} - impl AutoUpdater { pub fn get(cx: &mut AppContext) -> Option> { cx.default_global::().0.clone() @@ -423,6 +279,10 @@ impl AutoUpdater { })); } + pub fn current_version(&self) -> SemanticVersion { + self.current_version + } + pub fn status(&self) -> AutoUpdateStatus { self.status.clone() } @@ -646,7 +506,7 @@ impl AutoUpdater { Ok(()) } - fn set_should_show_update_notification( + pub fn set_should_show_update_notification( &self, should_show: bool, cx: &AppContext, @@ -668,7 +528,7 @@ impl AutoUpdater { }) } - fn should_show_update_notification(&self, cx: &AppContext) -> Task> { + pub fn should_show_update_notification(&self, cx: &AppContext) -> Task> { cx.background_executor().spawn(async move { Ok(KEY_VALUE_STORE .read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)? diff --git a/crates/auto_update_ui/Cargo.toml b/crates/auto_update_ui/Cargo.toml new file mode 100644 index 0000000000..1d62d295b7 --- /dev/null +++ b/crates/auto_update_ui/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "auto_update_ui" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/auto_update_ui.rs" + +[dependencies] +anyhow.workspace = true +auto_update.workspace = true +client.workspace = true +editor.workspace = true +gpui.workspace = true +http_client.workspace = true +markdown_preview.workspace = true +menu.workspace = true +release_channel.workspace = true +serde.workspace = true +serde_json.workspace = true +smol.workspace = true +util.workspace = true +workspace.workspace = true diff --git a/crates/auto_update_ui/LICENSE-GPL b/crates/auto_update_ui/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/auto_update_ui/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/auto_update_ui/src/auto_update_ui.rs b/crates/auto_update_ui/src/auto_update_ui.rs new file mode 100644 index 0000000000..9114375e88 --- /dev/null +++ b/crates/auto_update_ui/src/auto_update_ui.rs @@ -0,0 +1,147 @@ +mod update_notification; + +use auto_update::AutoUpdater; +use editor::{Editor, MultiBuffer}; +use gpui::{actions, prelude::*, AppContext, SharedString, View, ViewContext}; +use http_client::HttpClient; +use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView}; +use release_channel::{AppVersion, ReleaseChannel}; +use serde::Deserialize; +use smol::io::AsyncReadExt; +use util::ResultExt as _; +use workspace::notifications::NotificationId; +use workspace::Workspace; + +use crate::update_notification::UpdateNotification; + +actions!(auto_update, [ViewReleaseNotesLocally]); + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views(|workspace: &mut Workspace, _cx| { + workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| { + view_release_notes_locally(workspace, cx); + }); + }) + .detach(); +} + +#[derive(Deserialize)] +struct ReleaseNotesBody { + title: String, + release_notes: String, +} + +fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext) { + let release_channel = ReleaseChannel::global(cx); + + let url = match release_channel { + ReleaseChannel::Nightly => Some("https://github.com/zed-industries/zed/commits/nightly/"), + ReleaseChannel::Dev => Some("https://github.com/zed-industries/zed/commits/main/"), + _ => None, + }; + + if let Some(url) = url { + cx.open_url(url); + return; + } + + let version = AppVersion::global(cx).to_string(); + + let client = client::Client::global(cx).http_client(); + let url = client.build_url(&format!( + "/api/release_notes/v2/{}/{}", + release_channel.dev_name(), + version + )); + + let markdown = workspace + .app_state() + .languages + .language_for_name("Markdown"); + + workspace + .with_local_workspace(cx, move |_, cx| { + cx.spawn(|workspace, mut cx| async move { + let markdown = markdown.await.log_err(); + let response = client.get(&url, Default::default(), true).await; + let Some(mut response) = response.log_err() else { + return; + }; + + let mut body = Vec::new(); + response.body_mut().read_to_end(&mut body).await.ok(); + + let body: serde_json::Result = + serde_json::from_slice(body.as_slice()); + + if let Ok(body) = body { + workspace + .update(&mut cx, |workspace, cx| { + let project = workspace.project().clone(); + let buffer = project.update(cx, |project, cx| { + project.create_local_buffer("", markdown, cx) + }); + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, body.release_notes)], None, cx) + }); + let language_registry = project.read(cx).languages().clone(); + + let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + + let tab_description = SharedString::from(body.title.to_string()); + let editor = cx.new_view(|cx| { + Editor::for_multibuffer(buffer, Some(project), true, cx) + }); + let workspace_handle = workspace.weak_handle(); + let view: View = MarkdownPreviewView::new( + MarkdownPreviewMode::Default, + editor, + workspace_handle, + language_registry, + Some(tab_description), + cx, + ); + workspace.add_item_to_active_pane( + Box::new(view.clone()), + None, + true, + cx, + ); + cx.notify(); + }) + .log_err(); + } + }) + .detach(); + }) + .detach(); +} + +pub fn notify_of_any_new_update(cx: &mut ViewContext) -> Option<()> { + let updater = AutoUpdater::get(cx)?; + let version = updater.read(cx).current_version(); + let should_show_notification = updater.read(cx).should_show_update_notification(cx); + + cx.spawn(|workspace, mut cx| async move { + let should_show_notification = should_show_notification.await?; + if should_show_notification { + workspace.update(&mut cx, |workspace, cx| { + let workspace_handle = workspace.weak_handle(); + workspace.show_notification( + NotificationId::unique::(), + cx, + |cx| cx.new_view(|_| UpdateNotification::new(version, workspace_handle)), + ); + updater.update(cx, |updater, cx| { + updater + .set_should_show_update_notification(false, cx) + .detach_and_log_err(cx); + }); + })?; + } + anyhow::Ok(()) + }) + .detach(); + + None +} diff --git a/crates/auto_update/src/update_notification.rs b/crates/auto_update_ui/src/update_notification.rs similarity index 100% rename from crates/auto_update/src/update_notification.rs rename to crates/auto_update_ui/src/update_notification.rs diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 1cf9fa706d..11f618d196 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -343,7 +343,7 @@ fn init_test(cx: &mut AppContext) -> Model { release_channel::init(SemanticVersion::default(), cx); client::init_settings(cx); - let clock = Arc::new(FakeSystemClock::default()); + let clock = Arc::new(FakeSystemClock::new()); let http = FakeHttpClient::with_404_response(); let client = Client::new(clock, http.clone(), cx); let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx)); diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 9892011297..23716f0c69 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -42,7 +42,6 @@ serde_json.workspace = true settings.workspace = true sha2.workspace = true smol.workspace = true -sysinfo.workspace = true telemetry_events.workspace = true text.workspace = true thiserror.workspace = true diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 1e73c7be66..a20584fabd 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1067,6 +1067,8 @@ impl Client { let proxy = http.proxy().cloned(); let credentials = credentials.clone(); let rpc_url = self.rpc_url(http, release_channel); + let system_id = self.telemetry.system_id(); + let metrics_id = self.telemetry.metrics_id(); cx.background_executor().spawn(async move { use HttpOrHttps::*; @@ -1118,6 +1120,12 @@ impl Client { "x-zed-release-channel", HeaderValue::from_str(release_channel.map(|r| r.dev_name()).unwrap_or("unknown"))?, ); + if let Some(system_id) = system_id { + request_headers.insert("x-zed-system-id", HeaderValue::from_str(&system_id)?); + } + if let Some(metrics_id) = metrics_id { + request_headers.insert("x-zed-metrics-id", HeaderValue::from_str(&metrics_id)?); + } match url_scheme { Https => { @@ -1780,7 +1788,7 @@ mod tests { let user_id = 5; let client = cx.update(|cx| { Client::new( - Arc::new(FakeSystemClock::default()), + Arc::new(FakeSystemClock::new()), FakeHttpClient::with_404_response(), cx, ) @@ -1821,7 +1829,7 @@ mod tests { let user_id = 5; let client = cx.update(|cx| { Client::new( - Arc::new(FakeSystemClock::default()), + Arc::new(FakeSystemClock::new()), FakeHttpClient::with_404_response(), cx, ) @@ -1900,7 +1908,7 @@ mod tests { let dropped_auth_count = Arc::new(Mutex::new(0)); let client = cx.update(|cx| { Client::new( - Arc::new(FakeSystemClock::default()), + Arc::new(FakeSystemClock::new()), FakeHttpClient::with_404_response(), cx, ) @@ -1943,7 +1951,7 @@ mod tests { let user_id = 5; let client = cx.update(|cx| { Client::new( - Arc::new(FakeSystemClock::default()), + Arc::new(FakeSystemClock::new()), FakeHttpClient::with_404_response(), cx, ) @@ -2003,7 +2011,7 @@ mod tests { let user_id = 5; let client = cx.update(|cx| { Client::new( - Arc::new(FakeSystemClock::default()), + Arc::new(FakeSystemClock::new()), FakeHttpClient::with_404_response(), cx, ) @@ -2038,7 +2046,7 @@ mod tests { let user_id = 5; let client = cx.update(|cx| { Client::new( - Arc::new(FakeSystemClock::default()), + Arc::new(FakeSystemClock::new()), FakeHttpClient::with_404_response(), cx, ) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 27a49a9816..eef2a8215f 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -2,7 +2,6 @@ mod event_coalescer; use crate::{ChannelId, TelemetrySettings}; use anyhow::Result; -use chrono::{DateTime, Utc}; use clock::SystemClock; use collections::{HashMap, HashSet}; use futures::Future; @@ -15,12 +14,11 @@ use settings::{Settings, SettingsStore}; use sha2::{Digest, Sha256}; use std::fs::File; use std::io::Write; +use std::time::Instant; use std::{env, mem, path::PathBuf, sync::Arc, time::Duration}; -use sysinfo::{CpuRefreshKind, Pid, ProcessRefreshKind, RefreshKind, System}; use telemetry_events::{ - ActionEvent, AppEvent, AssistantEvent, CallEvent, CpuEvent, EditEvent, EditorEvent, Event, - EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent, ReplEvent, - SettingEvent, + ActionEvent, AppEvent, AssistantEvent, CallEvent, EditEvent, EditorEvent, Event, + EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, ReplEvent, SettingEvent, }; use util::{ResultExt, TryFutureExt}; use worktree::{UpdatedEntriesSet, WorktreeId}; @@ -46,7 +44,7 @@ struct TelemetryState { flush_events_task: Option>, log_file: Option, is_staff: Option, - first_event_date_time: Option>, + first_event_date_time: Option, event_coalescer: EventCoalescer, max_queue_size: usize, worktree_id_map: WorktreeIdMap, @@ -226,6 +224,8 @@ impl Telemetry { cx.background_executor() .spawn({ let state = state.clone(); + let os_version = os_version(); + state.lock().os_version = Some(os_version.clone()); async move { if let Some(tempfile) = File::create(Self::log_file_path()).log_err() { state.lock().log_file = Some(tempfile); @@ -293,48 +293,6 @@ impl Telemetry { state.session_id = Some(session_id); state.app_version = release_channel::AppVersion::global(cx).to_string(); state.os_name = os_name(); - - drop(state); - - let this = self.clone(); - cx.background_executor() - .spawn(async move { - let mut system = System::new_with_specifics( - RefreshKind::new().with_cpu(CpuRefreshKind::everything()), - ); - - let refresh_kind = ProcessRefreshKind::new().with_cpu().with_memory(); - let current_process = Pid::from_u32(std::process::id()); - system.refresh_processes_specifics( - sysinfo::ProcessesToUpdate::Some(&[current_process]), - refresh_kind, - ); - - // Waiting some amount of time before the first query is important to get a reasonable value - // https://docs.rs/sysinfo/0.29.10/sysinfo/trait.ProcessExt.html#tymethod.cpu_usage - const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(4 * 60); - - loop { - smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await; - - let current_process = Pid::from_u32(std::process::id()); - system.refresh_processes_specifics( - sysinfo::ProcessesToUpdate::Some(&[current_process]), - refresh_kind, - ); - let Some(process) = system.process(current_process) else { - log::error!( - "Failed to find own process {current_process:?} in system process table" - ); - // TODO: Fire an error telemetry event - return; - }; - - this.report_memory_event(process.memory(), process.virtual_memory()); - this.report_cpu_event(process.cpu_usage(), system.cpus().len() as u32); - } - }) - .detach(); } pub fn metrics_enabled(self: &Arc) -> bool { @@ -416,28 +374,6 @@ impl Telemetry { self.report_event(event) } - pub fn report_cpu_event(self: &Arc, usage_as_percentage: f32, core_count: u32) { - let event = Event::Cpu(CpuEvent { - usage_as_percentage, - core_count, - }); - - self.report_event(event) - } - - pub fn report_memory_event( - self: &Arc, - memory_in_bytes: u64, - virtual_memory_in_bytes: u64, - ) { - let event = Event::Memory(MemoryEvent { - memory_in_bytes, - virtual_memory_in_bytes, - }); - - self.report_event(event) - } - pub fn report_app_event(self: &Arc, operation: String) -> Event { let event = Event::App(AppEvent { operation }); @@ -469,7 +405,10 @@ impl Telemetry { if let Some((start, end, environment)) = period_data { let event = Event::Edit(EditEvent { - duration: end.timestamp_millis() - start.timestamp_millis(), + duration: end + .saturating_duration_since(start) + .min(Duration::from_secs(60 * 60 * 24)) + .as_millis() as i64, environment: environment.to_string(), is_via_ssh, }); @@ -567,9 +506,10 @@ impl Telemetry { let date_time = self.clock.utc_now(); let milliseconds_since_first_event = match state.first_event_date_time { - Some(first_event_date_time) => { - date_time.timestamp_millis() - first_event_date_time.timestamp_millis() - } + Some(first_event_date_time) => date_time + .saturating_duration_since(first_event_date_time) + .min(Duration::from_secs(60 * 60 * 24)) + .as_millis() as i64, None => { state.first_event_date_time = Some(date_time); 0 @@ -593,6 +533,10 @@ impl Telemetry { self.state.lock().metrics_id.clone() } + pub fn system_id(self: &Arc) -> Option> { + self.state.lock().system_id.clone() + } + pub fn installation_id(self: &Arc) -> Option> { self.state.lock().installation_id.clone() } @@ -702,7 +646,6 @@ pub fn calculate_json_checksum(json: &impl AsRef<[u8]>) -> Option { #[cfg(test)] mod tests { use super::*; - use chrono::TimeZone; use clock::FakeSystemClock; use gpui::TestAppContext; use http_client::FakeHttpClient; @@ -710,9 +653,7 @@ mod tests { #[gpui::test] fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) { init_test(cx); - let clock = Arc::new(FakeSystemClock::new( - Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(), - )); + let clock = Arc::new(FakeSystemClock::new()); let http = FakeHttpClient::with_200_response(); let system_id = Some("system_id".to_string()); let installation_id = Some("installation_id".to_string()); @@ -743,7 +684,7 @@ mod tests { Some(first_date_time) ); - clock.advance(chrono::Duration::milliseconds(100)); + clock.advance(Duration::from_millis(100)); let event = telemetry.report_app_event(operation.clone()); assert_eq!( @@ -759,7 +700,7 @@ mod tests { Some(first_date_time) ); - clock.advance(chrono::Duration::milliseconds(100)); + clock.advance(Duration::from_millis(100)); let event = telemetry.report_app_event(operation.clone()); assert_eq!( @@ -775,7 +716,7 @@ mod tests { Some(first_date_time) ); - clock.advance(chrono::Duration::milliseconds(100)); + clock.advance(Duration::from_millis(100)); // Adding a 4th event should cause a flush let event = telemetry.report_app_event(operation.clone()); @@ -796,9 +737,7 @@ mod tests { cx: &mut TestAppContext, ) { init_test(cx); - let clock = Arc::new(FakeSystemClock::new( - Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(), - )); + let clock = Arc::new(FakeSystemClock::new()); let http = FakeHttpClient::with_200_response(); let system_id = Some("system_id".to_string()); let installation_id = Some("installation_id".to_string()); diff --git a/crates/client/src/telemetry/event_coalescer.rs b/crates/client/src/telemetry/event_coalescer.rs index 33bcf492f6..e58112ac08 100644 --- a/crates/client/src/telemetry/event_coalescer.rs +++ b/crates/client/src/telemetry/event_coalescer.rs @@ -1,7 +1,6 @@ -use std::sync::Arc; use std::time; +use std::{sync::Arc, time::Instant}; -use chrono::{DateTime, Duration, Utc}; use clock::SystemClock; const COALESCE_TIMEOUT: time::Duration = time::Duration::from_secs(20); @@ -10,8 +9,8 @@ const SIMULATED_DURATION_FOR_SINGLE_EVENT: time::Duration = time::Duration::from #[derive(Debug, PartialEq)] struct PeriodData { environment: &'static str, - start: DateTime, - end: Option>, + start: Instant, + end: Option, } pub struct EventCoalescer { @@ -27,9 +26,8 @@ impl EventCoalescer { pub fn log_event( &mut self, environment: &'static str, - ) -> Option<(DateTime, DateTime, &'static str)> { + ) -> Option<(Instant, Instant, &'static str)> { let log_time = self.clock.utc_now(); - let coalesce_timeout = Duration::from_std(COALESCE_TIMEOUT).unwrap(); let Some(state) = &mut self.state else { self.state = Some(PeriodData { @@ -43,7 +41,7 @@ impl EventCoalescer { let period_end = state .end .unwrap_or(state.start + SIMULATED_DURATION_FOR_SINGLE_EVENT); - let within_timeout = log_time - period_end < coalesce_timeout; + let within_timeout = log_time - period_end < COALESCE_TIMEOUT; let environment_is_same = state.environment == environment; let should_coaelesce = !within_timeout || !environment_is_same; @@ -70,16 +68,13 @@ impl EventCoalescer { #[cfg(test)] mod tests { - use chrono::TimeZone; use clock::FakeSystemClock; use super::*; #[test] fn test_same_context_exceeding_timeout() { - let clock = Arc::new(FakeSystemClock::new( - Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(), - )); + let clock = Arc::new(FakeSystemClock::new()); let environment_1 = "environment_1"; let mut event_coalescer = EventCoalescer::new(clock.clone()); @@ -98,7 +93,7 @@ mod tests { }) ); - let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap(); + let within_timeout_adjustment = COALESCE_TIMEOUT / 2; // Ensure that many calls within the timeout don't start a new period for _ in 0..100 { @@ -118,7 +113,7 @@ mod tests { } let period_end = clock.utc_now(); - let exceed_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT * 2).unwrap(); + let exceed_timeout_adjustment = COALESCE_TIMEOUT * 2; // Logging an event exceeding the timeout should start a new period clock.advance(exceed_timeout_adjustment); let new_period_start = clock.utc_now(); @@ -137,9 +132,7 @@ mod tests { #[test] fn test_different_environment_under_timeout() { - let clock = Arc::new(FakeSystemClock::new( - Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(), - )); + let clock = Arc::new(FakeSystemClock::new()); let environment_1 = "environment_1"; let mut event_coalescer = EventCoalescer::new(clock.clone()); @@ -158,7 +151,7 @@ mod tests { }) ); - let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap(); + let within_timeout_adjustment = COALESCE_TIMEOUT / 2; clock.advance(within_timeout_adjustment); let period_end = clock.utc_now(); let period_data = event_coalescer.log_event(environment_1); @@ -193,9 +186,7 @@ mod tests { #[test] fn test_switching_environment_while_within_timeout() { - let clock = Arc::new(FakeSystemClock::new( - Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(), - )); + let clock = Arc::new(FakeSystemClock::new()); let environment_1 = "environment_1"; let mut event_coalescer = EventCoalescer::new(clock.clone()); @@ -214,7 +205,7 @@ mod tests { }) ); - let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap(); + let within_timeout_adjustment = COALESCE_TIMEOUT / 2; clock.advance(within_timeout_adjustment); let period_end = clock.utc_now(); let environment_2 = "environment_2"; @@ -240,9 +231,7 @@ mod tests { #[test] fn test_switching_environment_while_exceeding_timeout() { - let clock = Arc::new(FakeSystemClock::new( - Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(), - )); + let clock = Arc::new(FakeSystemClock::new()); let environment_1 = "environment_1"; let mut event_coalescer = EventCoalescer::new(clock.clone()); @@ -261,7 +250,7 @@ mod tests { }) ); - let exceed_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT * 2).unwrap(); + let exceed_timeout_adjustment = COALESCE_TIMEOUT * 2; clock.advance(exceed_timeout_adjustment); let period_end = clock.utc_now(); let environment_2 = "environment_2"; diff --git a/crates/clock/Cargo.toml b/crates/clock/Cargo.toml index 699a50e70d..b6f28741c3 100644 --- a/crates/clock/Cargo.toml +++ b/crates/clock/Cargo.toml @@ -16,7 +16,6 @@ doctest = false test-support = ["dep:parking_lot"] [dependencies] -chrono.workspace = true parking_lot = { workspace = true, optional = true } serde.workspace = true smallvec.workspace = true diff --git a/crates/clock/src/system_clock.rs b/crates/clock/src/system_clock.rs index a462ffc35b..b8e50d0b27 100644 --- a/crates/clock/src/system_clock.rs +++ b/crates/clock/src/system_clock.rs @@ -1,21 +1,21 @@ -use chrono::{DateTime, Utc}; +use std::time::Instant; pub trait SystemClock: Send + Sync { /// Returns the current date and time in UTC. - fn utc_now(&self) -> DateTime; + fn utc_now(&self) -> Instant; } pub struct RealSystemClock; impl SystemClock for RealSystemClock { - fn utc_now(&self) -> DateTime { - Utc::now() + fn utc_now(&self) -> Instant { + Instant::now() } } #[cfg(any(test, feature = "test-support"))] pub struct FakeSystemClockState { - now: DateTime, + now: Instant, } #[cfg(any(test, feature = "test-support"))] @@ -24,36 +24,30 @@ pub struct FakeSystemClock { state: parking_lot::Mutex, } -#[cfg(any(test, feature = "test-support"))] -impl Default for FakeSystemClock { - fn default() -> Self { - Self::new(Utc::now()) - } -} - #[cfg(any(test, feature = "test-support"))] impl FakeSystemClock { - pub fn new(now: DateTime) -> Self { - let state = FakeSystemClockState { now }; + pub fn new() -> Self { + let state = FakeSystemClockState { + now: Instant::now(), + }; Self { state: parking_lot::Mutex::new(state), } } - pub fn set_now(&self, now: DateTime) { + pub fn set_now(&self, now: Instant) { self.state.lock().now = now; } - /// Advances the [`FakeSystemClock`] by the specified [`Duration`](chrono::Duration). - pub fn advance(&self, duration: chrono::Duration) { + pub fn advance(&self, duration: std::time::Duration) { self.state.lock().now += duration; } } #[cfg(any(test, feature = "test-support"))] impl SystemClock for FakeSystemClock { - fn utc_now(&self) -> DateTime { + fn utc_now(&self) -> Instant { self.state.lock().now } } diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index a69eb53740..d3da1c2816 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -90,6 +90,7 @@ collections = { workspace = true, features = ["test-support"] } ctor.workspace = true editor = { workspace = true, features = ["test-support"] } env_logger.workspace = true +extension.workspace = true file_finder.workspace = true fs = { workspace = true, features = ["test-support"] } git = { workspace = true, features = ["test-support"] } diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 46ca5906c5..7adf17ac06 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -61,6 +61,39 @@ impl std::fmt::Display for CloudflareIpCountryHeader { } } +pub struct SystemIdHeader(String); + +impl Header for SystemIdHeader { + fn name() -> &'static HeaderName { + static SYSTEM_ID_HEADER: OnceLock = OnceLock::new(); + SYSTEM_ID_HEADER.get_or_init(|| HeaderName::from_static("x-zed-system-id")) + } + + fn decode<'i, I>(values: &mut I) -> Result + where + Self: Sized, + I: Iterator, + { + let system_id = values + .next() + .ok_or_else(axum::headers::Error::invalid)? + .to_str() + .map_err(|_| axum::headers::Error::invalid())?; + + Ok(Self(system_id.to_string())) + } + + fn encode>(&self, _values: &mut E) { + unimplemented!() + } +} + +impl std::fmt::Display for SystemIdHeader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + pub fn routes(rpc_server: Arc) -> Router<(), Body> { Router::new() .route("/user", get(get_authenticated_user)) diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index 2357d03bab..b5cd920fb3 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -15,6 +15,7 @@ use chrono::Duration; use rpc::ExtensionMetadata; use semantic_version::SemanticVersion; use serde::{Deserialize, Serialize, Serializer}; +use serde_json::json; use sha2::{Digest, Sha256}; use std::sync::{Arc, OnceLock}; use telemetry_events::{ @@ -417,9 +418,8 @@ pub async fn post_events( if let Some(kinesis_client) = app.kinesis_client.clone() { if let Some(stream) = app.config.kinesis_stream.clone() { let mut request = kinesis_client.put_records().stream_name(stream); - for row in for_snowflake(request_body.clone(), first_event_at) { + for row in for_snowflake(request_body.clone(), first_event_at, country_code.clone()) { if let Some(data) = serde_json::to_vec(&row).log_err() { - println!("{}", String::from_utf8_lossy(&data)); request = request.records( aws_sdk_kinesis::types::PutRecordsRequestEntry::builder() .partition_key(request_body.system_id.clone().unwrap_or_default()) @@ -483,20 +483,7 @@ pub async fn post_events( checksum_matched, )) } - Event::Cpu(event) => to_upload.cpu_events.push(CpuEventRow::from_event( - event.clone(), - wrapper, - &request_body, - first_event_at, - checksum_matched, - )), - Event::Memory(event) => to_upload.memory_events.push(MemoryEventRow::from_event( - event.clone(), - wrapper, - &request_body, - first_event_at, - checksum_matched, - )), + Event::Cpu(_) | Event::Memory(_) => continue, Event::App(event) => to_upload.app_events.push(AppEventRow::from_event( event.clone(), wrapper, @@ -947,6 +934,7 @@ pub struct CpuEventRow { } impl CpuEventRow { + #[allow(unused)] fn from_event( event: CpuEvent, wrapper: &EventWrapper, @@ -1001,6 +989,7 @@ pub struct MemoryEventRow { } impl MemoryEventRow { + #[allow(unused)] fn from_event( event: MemoryEvent, wrapper: &EventWrapper, @@ -1392,155 +1381,246 @@ pub fn calculate_json_checksum(app: Arc, json: &impl AsRef<[u8]>) -> O fn for_snowflake( body: EventRequestBody, first_event_at: chrono::DateTime, + country_code: Option, ) -> impl Iterator { - body.events.into_iter().map(move |event| SnowflakeRow { - event: match &event.event { - Event::Editor(editor_event) => format!("editor_{}", editor_event.operation), - Event::InlineCompletion(inline_completion_event) => format!( - "inline_completion_{}", - if inline_completion_event.suggestion_accepted { - "accept " - } else { - "discard" - } + body.events.into_iter().flat_map(move |event| { + let timestamp = + first_event_at + Duration::milliseconds(event.milliseconds_since_first_event); + let (event_type, mut event_properties) = match &event.event { + Event::Editor(e) => ( + match e.operation.as_str() { + "open" => "Editor Opened".to_string(), + "save" => "Editor Saved".to_string(), + _ => format!("Unknown Editor Event: {}", e.operation), + }, + serde_json::to_value(e).unwrap(), ), - Event::Call(call_event) => format!("call_{}", call_event.operation.replace(" ", "_")), - Event::Assistant(assistant_event) => { + Event::InlineCompletion(e) => ( format!( - "assistant_{}", - match assistant_event.phase { - telemetry_events::AssistantPhase::Response => "response", - telemetry_events::AssistantPhase::Invoked => "invoke", - telemetry_events::AssistantPhase::Accepted => "accept", - telemetry_events::AssistantPhase::Rejected => "reject", + "Inline Completion {}", + if e.suggestion_accepted { + "Accepted" + } else { + "Discarded" } - ) + ), + serde_json::to_value(e).unwrap(), + ), + Event::Call(e) => { + let event_type = match e.operation.trim() { + "unshare project" => "Project Unshared".to_string(), + "open channel notes" => "Channel Notes Opened".to_string(), + "share project" => "Project Shared".to_string(), + "join channel" => "Channel Joined".to_string(), + "hang up" => "Call Ended".to_string(), + "accept incoming" => "Incoming Call Accepted".to_string(), + "invite" => "Participant Invited".to_string(), + "disable microphone" => "Microphone Disabled".to_string(), + "enable microphone" => "Microphone Enabled".to_string(), + "enable screen share" => "Screen Share Enabled".to_string(), + "disable screen share" => "Screen Share Disabled".to_string(), + "decline incoming" => "Incoming Call Declined".to_string(), + _ => format!("Unknown Call Event: {}", e.operation), + }; + + (event_type, serde_json::to_value(e).unwrap()) } - Event::Cpu(_) => "system_cpu".to_string(), - Event::Memory(_) => "system_memory".to_string(), - Event::App(app_event) => app_event.operation.replace(" ", "_"), - Event::Setting(_) => "setting_change".to_string(), - Event::Extension(_) => "extension_load".to_string(), - Event::Edit(_) => "edit".to_string(), - Event::Action(_) => "command_palette_action".to_string(), - Event::Repl(_) => "repl".to_string(), - }, - system_id: body.system_id.clone(), - timestamp: first_event_at + Duration::milliseconds(event.milliseconds_since_first_event), - data: SnowflakeData { - installation_id: body.installation_id.clone(), - session_id: body.session_id.clone(), - metrics_id: body.metrics_id.clone(), - is_staff: body.is_staff, - app_version: body.app_version.clone(), - os_name: body.os_name.clone(), - os_version: body.os_version.clone(), - architecture: body.architecture.clone(), - release_channel: body.release_channel.clone(), - signed_in: event.signed_in, - editor_event: match &event.event { - Event::Editor(editor_event) => Some(editor_event.clone()), - _ => None, - }, - inline_completion_event: match &event.event { - Event::InlineCompletion(inline_completion_event) => { - Some(inline_completion_event.clone()) - } - _ => None, - }, - call_event: match &event.event { - Event::Call(call_event) => Some(call_event.clone()), - _ => None, - }, - assistant_event: match &event.event { - Event::Assistant(assistant_event) => Some(assistant_event.clone()), - _ => None, - }, - cpu_event: match &event.event { - Event::Cpu(cpu_event) => Some(cpu_event.clone()), - _ => None, - }, - memory_event: match &event.event { - Event::Memory(memory_event) => Some(memory_event.clone()), - _ => None, - }, - app_event: match &event.event { - Event::App(app_event) => Some(app_event.clone()), - _ => None, - }, - setting_event: match &event.event { - Event::Setting(setting_event) => Some(setting_event.clone()), - _ => None, - }, - extension_event: match &event.event { - Event::Extension(extension_event) => Some(extension_event.clone()), - _ => None, - }, - edit_event: match &event.event { - Event::Edit(edit_event) => Some(edit_event.clone()), - _ => None, - }, - repl_event: match &event.event { - Event::Repl(repl_event) => Some(repl_event.clone()), - _ => None, - }, - action_event: match event.event { - Event::Action(action_event) => Some(action_event.clone()), - _ => None, - }, - }, + Event::Assistant(e) => ( + match e.phase { + telemetry_events::AssistantPhase::Response => "Assistant Responded".to_string(), + telemetry_events::AssistantPhase::Invoked => "Assistant Invoked".to_string(), + telemetry_events::AssistantPhase::Accepted => { + "Assistant Response Accepted".to_string() + } + telemetry_events::AssistantPhase::Rejected => { + "Assistant Response Rejected".to_string() + } + }, + serde_json::to_value(e).unwrap(), + ), + Event::Cpu(_) | Event::Memory(_) => return None, + Event::App(e) => { + let mut properties = json!({}); + let event_type = match e.operation.trim() { + // App + "open" => "App Opened".to_string(), + "first open" => "App First Opened".to_string(), + "first open for release channel" => { + "App First Opened For Release Channel".to_string() + } + "close" => "App Closed".to_string(), + + // Project + "open project" => "Project Opened".to_string(), + "open node project" => { + properties["project_type"] = json!("node"); + "Project Opened".to_string() + } + "open pnpm project" => { + properties["project_type"] = json!("pnpm"); + "Project Opened".to_string() + } + "open yarn project" => { + properties["project_type"] = json!("yarn"); + "Project Opened".to_string() + } + + // SSH + "create ssh server" => "SSH Server Created".to_string(), + "create ssh project" => "SSH Project Created".to_string(), + "open ssh project" => "SSH Project Opened".to_string(), + + // Welcome Page + "welcome page: change keymap" => "Welcome Keymap Changed".to_string(), + "welcome page: change theme" => "Welcome Theme Changed".to_string(), + "welcome page: close" => "Welcome Page Closed".to_string(), + "welcome page: edit settings" => "Welcome Settings Edited".to_string(), + "welcome page: install cli" => "Welcome CLI Installed".to_string(), + "welcome page: open" => "Welcome Page Opened".to_string(), + "welcome page: open extensions" => "Welcome Extensions Page Opened".to_string(), + "welcome page: sign in to copilot" => "Welcome Copilot Signed In".to_string(), + "welcome page: toggle diagnostic telemetry" => { + "Welcome Diagnostic Telemetry Toggled".to_string() + } + "welcome page: toggle metric telemetry" => { + "Welcome Metric Telemetry Toggled".to_string() + } + "welcome page: toggle vim" => "Welcome Vim Mode Toggled".to_string(), + "welcome page: view docs" => "Welcome Documentation Viewed".to_string(), + + // Extensions + "extensions page: open" => "Extensions Page Opened".to_string(), + "extensions: install extension" => "Extension Installed".to_string(), + "extensions: uninstall extension" => "Extension Uninstalled".to_string(), + + // Misc + "markdown preview: open" => "Markdown Preview Opened".to_string(), + "project diagnostics: open" => "Project Diagnostics Opened".to_string(), + "project search: open" => "Project Search Opened".to_string(), + "repl sessions: open" => "REPL Session Started".to_string(), + + // Feature Upsell + "feature upsell: toggle vim" => { + properties["source"] = json!("Feature Upsell"); + "Vim Mode Toggled".to_string() + } + _ => e + .operation + .strip_prefix("feature upsell: viewed docs (") + .and_then(|s| s.strip_suffix(')')) + .map_or_else( + || format!("Unknown App Event: {}", e.operation), + |docs_url| { + properties["url"] = json!(docs_url); + properties["source"] = json!("Feature Upsell"); + "Documentation Viewed".to_string() + }, + ), + }; + (event_type, properties) + } + Event::Setting(e) => ( + "Settings Changed".to_string(), + serde_json::to_value(e).unwrap(), + ), + Event::Extension(e) => ( + "Extension Loaded".to_string(), + serde_json::to_value(e).unwrap(), + ), + Event::Edit(e) => ( + "Editor Edited".to_string(), + serde_json::to_value(e).unwrap(), + ), + Event::Action(e) => ( + "Action Invoked".to_string(), + serde_json::to_value(e).unwrap(), + ), + Event::Repl(e) => ( + "Kernel Status Changed".to_string(), + serde_json::to_value(e).unwrap(), + ), + }; + + if let serde_json::Value::Object(ref mut map) = event_properties { + map.insert("app_version".to_string(), body.app_version.clone().into()); + map.insert("os_name".to_string(), body.os_name.clone().into()); + map.insert("os_version".to_string(), body.os_version.clone().into()); + map.insert("architecture".to_string(), body.architecture.clone().into()); + map.insert( + "release_channel".to_string(), + body.release_channel.clone().into(), + ); + map.insert("signed_in".to_string(), event.signed_in.into()); + if let Some(country_code) = country_code.as_ref() { + map.insert("country".to_string(), country_code.clone().into()); + } + } + + // NOTE: most amplitude user properties are read out of our event_properties + // dictionary. See https://app.amplitude.com/data/zed/Zed/sources/detail/production/falcon%3A159998 + // for how that is configured. + let user_properties = Some(serde_json::json!({ + "is_staff": body.is_staff, + })); + + Some(SnowflakeRow { + time: timestamp, + user_id: body.metrics_id.clone(), + device_id: body.system_id.clone(), + event_type, + event_properties, + user_properties, + insert_id: Some(Uuid::new_v4().to_string()), + }) }) } -#[derive(Serialize, Deserialize)] -struct SnowflakeRow { - pub event: String, - pub system_id: Option, - pub timestamp: chrono::DateTime, - pub data: SnowflakeData, +#[derive(Serialize, Deserialize, Debug)] +pub struct SnowflakeRow { + pub time: chrono::DateTime, + pub user_id: Option, + pub device_id: Option, + pub event_type: String, + pub event_properties: serde_json::Value, + pub user_properties: Option, + pub insert_id: Option, } -#[derive(Serialize, Deserialize)] -struct SnowflakeData { - /// Identifier unique to each Zed installation (differs for stable, preview, dev) - pub installation_id: Option, - /// Identifier unique to each logged in Zed user (randomly generated on first sign in) - /// Identifier unique to each Zed session (differs for each time you open Zed) - pub session_id: Option, - pub metrics_id: Option, - /// True for Zed staff, otherwise false - pub is_staff: Option, - /// Zed version number - pub app_version: String, - pub os_name: String, - pub os_version: Option, - pub architecture: String, - /// Zed release channel (stable, preview, dev) - pub release_channel: Option, - pub signed_in: bool, +impl SnowflakeRow { + pub fn new( + event_type: impl Into, + metrics_id: Option, + is_staff: bool, + system_id: Option, + event_properties: serde_json::Value, + ) -> Self { + Self { + time: chrono::Utc::now(), + event_type: event_type.into(), + device_id: system_id, + user_id: metrics_id.map(|id| id.to_string()), + insert_id: Some(uuid::Uuid::new_v4().to_string()), + event_properties, + user_properties: Some(json!({"is_staff": is_staff})), + } + } - #[serde(flatten)] - pub editor_event: Option, - #[serde(flatten)] - pub inline_completion_event: Option, - #[serde(flatten)] - pub call_event: Option, - #[serde(flatten)] - pub assistant_event: Option, - #[serde(flatten)] - pub cpu_event: Option, - #[serde(flatten)] - pub memory_event: Option, - #[serde(flatten)] - pub app_event: Option, - #[serde(flatten)] - pub setting_event: Option, - #[serde(flatten)] - pub extension_event: Option, - #[serde(flatten)] - pub edit_event: Option, - #[serde(flatten)] - pub repl_event: Option, - #[serde(flatten)] - pub action_event: Option, + pub async fn write( + self, + client: &Option, + stream: &Option, + ) -> anyhow::Result<()> { + let Some((client, stream)) = client.as_ref().zip(stream.as_ref()) else { + return Ok(()); + }; + let row = serde_json::to_vec(&self)?; + client + .put_record() + .stream_name(stream) + .partition_key(&self.user_id.unwrap_or_default()) + .data(row.into()) + .send() + .await?; + Ok(()) + } } diff --git a/crates/collab/src/cents.rs b/crates/collab/src/cents.rs index defbcea4e2..a05971f141 100644 --- a/crates/collab/src/cents.rs +++ b/crates/collab/src/cents.rs @@ -1,3 +1,5 @@ +use serde::Serialize; + /// A number of cents. #[derive( Debug, @@ -12,6 +14,7 @@ derive_more::AddAssign, derive_more::Sub, derive_more::SubAssign, + Serialize, )] pub struct Cents(pub u32); diff --git a/crates/collab/src/llm.rs b/crates/collab/src/llm.rs index fa48ec95ea..603b76db73 100644 --- a/crates/collab/src/llm.rs +++ b/crates/collab/src/llm.rs @@ -3,9 +3,11 @@ pub mod db; mod telemetry; mod token; +use crate::api::events::SnowflakeRow; +use crate::api::CloudflareIpCountryHeader; +use crate::build_kinesis_client; use crate::{ - api::CloudflareIpCountryHeader, build_clickhouse_client, db::UserId, executor::Executor, Cents, - Config, Error, Result, + build_clickhouse_client, db::UserId, executor::Executor, Cents, Config, Error, Result, }; use anyhow::{anyhow, Context as _}; use authorization::authorize_access_to_language_model; @@ -28,6 +30,7 @@ use rpc::{ proto::Plan, LanguageModelProvider, PerformCompletionParams, EXPIRED_LLM_TOKEN_HEADER_NAME, }; use rpc::{ListModelsResponse, MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME}; +use serde_json::json; use std::{ pin::Pin, sync::Arc, @@ -45,6 +48,7 @@ pub struct LlmState { pub executor: Executor, pub db: Arc, pub http_client: ReqwestClient, + pub kinesis_client: Option, pub clickhouse_client: Option, active_user_count_by_model: RwLock, ActiveUserCount)>>, @@ -77,6 +81,11 @@ impl LlmState { executor, db, http_client, + kinesis_client: if config.kinesis_access_key.is_some() { + build_kinesis_client(&config).await.log_err() + } else { + None + }, clickhouse_client: config .clickhouse_url .as_ref() @@ -521,25 +530,50 @@ async fn check_usage_limit( UsageMeasure::TokensPerDay => "tokens_per_day", }; - if let Some(client) = state.clickhouse_client.as_ref() { - tracing::info!( - target: "user rate limit", - user_id = claims.user_id, - login = claims.github_user_login, - authn.jti = claims.jti, - is_staff = claims.is_staff, - provider = provider.to_string(), - model = model.name, - requests_this_minute = usage.requests_this_minute, - tokens_this_minute = usage.tokens_this_minute, - tokens_this_day = usage.tokens_this_day, - users_in_recent_minutes = users_in_recent_minutes, - users_in_recent_days = users_in_recent_days, - max_requests_per_minute = per_user_max_requests_per_minute, - max_tokens_per_minute = per_user_max_tokens_per_minute, - max_tokens_per_day = per_user_max_tokens_per_day, - ); + tracing::info!( + target: "user rate limit", + user_id = claims.user_id, + login = claims.github_user_login, + authn.jti = claims.jti, + is_staff = claims.is_staff, + provider = provider.to_string(), + model = model.name, + requests_this_minute = usage.requests_this_minute, + tokens_this_minute = usage.tokens_this_minute, + tokens_this_day = usage.tokens_this_day, + users_in_recent_minutes = users_in_recent_minutes, + users_in_recent_days = users_in_recent_days, + max_requests_per_minute = per_user_max_requests_per_minute, + max_tokens_per_minute = per_user_max_tokens_per_minute, + max_tokens_per_day = per_user_max_tokens_per_day, + ); + SnowflakeRow::new( + "Language Model Rate Limited", + claims.metrics_id, + claims.is_staff, + claims.system_id.clone(), + json!({ + "usage": usage, + "users_in_recent_minutes": users_in_recent_minutes, + "users_in_recent_days": users_in_recent_days, + "max_requests_per_minute": per_user_max_requests_per_minute, + "max_tokens_per_minute": per_user_max_tokens_per_minute, + "max_tokens_per_day": per_user_max_tokens_per_day, + "plan": match claims.plan { + Plan::Free => "free".to_string(), + Plan::ZedPro => "zed_pro".to_string(), + }, + "model": model.name.clone(), + "provider": provider.to_string(), + "usage_measure": resource.to_string(), + }), + ) + .write(&state.kinesis_client, &state.config.kinesis_stream) + .await + .log_err(); + + if let Some(client) = state.clickhouse_client.as_ref() { report_llm_rate_limit( client, LlmRateLimitEventRow { @@ -652,6 +686,27 @@ impl Drop for TokenCountingStream { tokens_this_minute = usage.tokens_this_minute, ); + let properties = json!({ + "plan": match claims.plan { + Plan::Free => "free".to_string(), + Plan::ZedPro => "zed_pro".to_string(), + }, + "model": model, + "provider": provider, + "usage": usage, + "tokens": tokens + }); + SnowflakeRow::new( + "Language Model Used", + claims.metrics_id, + claims.is_staff, + claims.system_id.clone(), + properties, + ) + .write(&state.kinesis_client, &state.config.kinesis_stream) + .await + .log_err(); + if let Some(clickhouse_client) = state.clickhouse_client.as_ref() { report_llm_usage( clickhouse_client, diff --git a/crates/collab/src/llm/db/queries/usages.rs b/crates/collab/src/llm/db/queries/usages.rs index f262821743..27e8039f54 100644 --- a/crates/collab/src/llm/db/queries/usages.rs +++ b/crates/collab/src/llm/db/queries/usages.rs @@ -9,7 +9,7 @@ use strum::IntoEnumIterator as _; use super::*; -#[derive(Debug, PartialEq, Clone, Copy, Default)] +#[derive(Debug, PartialEq, Clone, Copy, Default, serde::Serialize)] pub struct TokenUsage { pub input: usize, pub input_cache_creation: usize, @@ -23,7 +23,7 @@ impl TokenUsage { } } -#[derive(Debug, PartialEq, Clone, Copy)] +#[derive(Debug, PartialEq, Clone, Copy, serde::Serialize)] pub struct Usage { pub requests_this_minute: usize, pub tokens_this_minute: usize, diff --git a/crates/collab/src/llm/token.rs b/crates/collab/src/llm/token.rs index 35f7cf26e7..7e0706e2d5 100644 --- a/crates/collab/src/llm/token.rs +++ b/crates/collab/src/llm/token.rs @@ -8,6 +8,7 @@ use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; use std::time::Duration; use thiserror::Error; +use uuid::Uuid; #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -16,6 +17,10 @@ pub struct LlmTokenClaims { pub exp: u64, pub jti: String, pub user_id: u64, + #[serde(default)] + pub system_id: Option, + #[serde(default)] + pub metrics_id: Option, pub github_user_login: String, pub is_staff: bool, pub has_llm_closed_beta_feature_flag: bool, @@ -36,6 +41,7 @@ impl LlmTokenClaims { has_llm_closed_beta_feature_flag: bool, has_llm_subscription: bool, plan: rpc::proto::Plan, + system_id: Option, config: &Config, ) -> Result { let secret = config @@ -49,6 +55,8 @@ impl LlmTokenClaims { exp: (now + LLM_TOKEN_LIFETIME).timestamp() as u64, jti: uuid::Uuid::new_v4().to_string(), user_id: user.id.to_proto(), + system_id, + metrics_id: Some(user.metrics_id), github_user_login: user.github_login.clone(), is_staff, has_llm_closed_beta_feature_flag, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 02bbe95d92..c8691da02c 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1,6 +1,6 @@ mod connection_pool; -use crate::api::CloudflareIpCountryHeader; +use crate::api::{CloudflareIpCountryHeader, SystemIdHeader}; use crate::llm::LlmTokenClaims; use crate::{ auth, @@ -137,6 +137,7 @@ struct Session { /// The GeoIP country code for the user. #[allow(unused)] geoip_country_code: Option, + system_id: Option, _executor: Executor, } @@ -683,6 +684,7 @@ impl Server { principal: Principal, zed_version: ZedVersion, geoip_country_code: Option, + system_id: Option, send_connection_id: Option>, executor: Executor, ) -> impl Future { @@ -738,6 +740,7 @@ impl Server { app_state: this.app_state.clone(), http_client, geoip_country_code, + system_id, _executor: executor.clone(), supermaven_client, }; @@ -1057,6 +1060,7 @@ pub fn routes(server: Arc) -> Router<(), Body> { .layer(Extension(server)) } +#[allow(clippy::too_many_arguments)] pub async fn handle_websocket_request( TypedHeader(ProtocolVersion(protocol_version)): TypedHeader, app_version_header: Option>, @@ -1064,6 +1068,7 @@ pub async fn handle_websocket_request( Extension(server): Extension>, Extension(principal): Extension, country_code_header: Option>, + system_id_header: Option>, ws: WebSocketUpgrade, ) -> axum::response::Response { if protocol_version != rpc::PROTOCOL_VERSION { @@ -1105,6 +1110,7 @@ pub async fn handle_websocket_request( principal, version, country_code_header.map(|header| header.to_string()), + system_id_header.map(|header| header.to_string()), None, Executor::Production, ) @@ -4067,12 +4073,18 @@ async fn get_llm_api_token( Err(anyhow!("terms of service not accepted"))? } - let mut account_created_at = user.created_at; - if let Some(github_created_at) = user.github_user_created_at { - account_created_at = account_created_at.min(github_created_at); - } - if Utc::now().naive_utc() - account_created_at < MIN_ACCOUNT_AGE_FOR_LLM_USE { - Err(anyhow!("account too young"))? + let has_llm_subscription = session.has_llm_subscription(&db).await?; + + let bypass_account_age_check = + has_llm_subscription || flags.iter().any(|flag| flag == "bypass-account-age-check"); + if !bypass_account_age_check { + let mut account_created_at = user.created_at; + if let Some(github_created_at) = user.github_user_created_at { + account_created_at = account_created_at.min(github_created_at); + } + if Utc::now().naive_utc() - account_created_at < MIN_ACCOUNT_AGE_FOR_LLM_USE { + Err(anyhow!("account too young"))? + } } let billing_preferences = db.get_billing_preferences(user.id).await?; @@ -4082,8 +4094,9 @@ async fn get_llm_api_token( session.is_staff(), billing_preferences, has_llm_closed_beta_feature_flag, - session.has_llm_subscription(&db).await?, + has_llm_subscription, session.current_plan(&db).await?, + session.system_id.clone(), &session.app_state.config, )?; response.send(proto::GetLlmTokenResponse { token })?; diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index 47f6a38073..1f39190d75 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -835,7 +835,7 @@ impl RandomizedTest for ProjectCollaborationTest { .map_ok(|_| ()) .boxed(), LspRequestKind::CodeAction => project - .code_actions(&buffer, offset..offset, cx) + .code_actions(&buffer, offset..offset, None, cx) .map(|_| Ok(())) .boxed(), LspRequestKind::Definition => project @@ -1323,11 +1323,8 @@ impl RandomizedTest for ProjectCollaborationTest { match (host_file, guest_file) { (Some(host_file), Some(guest_file)) => { assert_eq!(guest_file.path(), host_file.path()); - assert_eq!(guest_file.is_deleted(), host_file.is_deleted()); - assert_eq!( - guest_file.mtime(), - host_file.mtime(), - "guest {} mtime does not match host {} for path {:?} in project {}", + assert_eq!(guest_file.disk_state(), host_file.disk_state(), + "guest {} disk_state does not match host {} for path {:?} in project {}", guest_user_id, host_user_id, guest_file.path(), diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index 00f52e9972..5b8d57a12a 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -1,6 +1,7 @@ use crate::tests::TestServer; use call::ActiveCall; use collections::HashSet; +use extension::ExtensionHostProxy; use fs::{FakeFs, Fs as _}; use futures::StreamExt as _; use gpui::{BackgroundExecutor, Context as _, SemanticVersion, TestAppContext, UpdateGlobal as _}; @@ -81,6 +82,7 @@ async fn test_sharing_an_ssh_remote_project( http_client: remote_http_client, node_runtime: node, languages, + extension_host_proxy: Arc::new(ExtensionHostProxy::new()), }, cx, ) @@ -243,6 +245,7 @@ async fn test_ssh_collaboration_git_branches( http_client: remote_http_client, node_runtime: node, languages, + extension_host_proxy: Arc::new(ExtensionHostProxy::new()), }, cx, ) @@ -400,6 +403,7 @@ async fn test_ssh_collaboration_formatting_with_prettier( http_client: remote_http_client, node_runtime: NodeRuntime::unavailable(), languages, + extension_host_proxy: Arc::new(ExtensionHostProxy::new()), }, cx, ) diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index e1893e06aa..7b796b4580 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -168,7 +168,7 @@ impl TestServer { client::init_settings(cx); }); - let clock = Arc::new(FakeSystemClock::default()); + let clock = Arc::new(FakeSystemClock::new()); let http = FakeHttpClient::with_404_response(); let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await { @@ -244,6 +244,7 @@ impl TestServer { Principal::User(user), ZedVersion(SemanticVersion::new(1, 0, 0)), None, + None, Some(connection_id_tx), Executor::Deterministic(cx.background_executor().clone()), )) diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index cd00e13206..3cc8f25b18 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -58,12 +58,11 @@ settings.workspace = true smallvec.workspace = true story = { workspace = true, optional = true } theme.workspace = true -time_format.workspace = true time.workspace = true +time_format.workspace = true title_bar.workspace = true ui.workspace = true util.workspace = true -vcs_menu.workspace = true workspace.workspace = true [dev-dependencies] diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 5a79f364ff..b330b5b531 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -23,7 +23,7 @@ use std::{sync::Arc, time::Duration}; use time::{OffsetDateTime, UtcOffset}; use ui::{ prelude::*, Avatar, Button, ContextMenu, IconButton, IconName, KeyBinding, Label, PopoverMenu, - TabBar, Tooltip, + Tab, TabBar, Tooltip, }; use util::{ResultExt, TryFutureExt}; use workspace::{ @@ -939,7 +939,7 @@ impl Render for ChatPanel { TabBar::new("chat_header").child( h_flex() .w_full() - .h(rems(ui::Tab::CONTAINER_HEIGHT_IN_REMS)) + .h(Tab::container_height(cx)) .px_2() .child(Label::new( self.active_chat diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 2baaa01490..67c4ad6dad 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -33,7 +33,6 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { notification_panel::init(cx); notifications::init(app_state, cx); title_bar::init(cx); - vcs_menu::init(cx); } fn notification_window_options( diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 33ca5a2952..410b90c727 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -19,7 +19,9 @@ use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use std::{sync::Arc, time::Duration}; use time::{OffsetDateTime, UtcOffset}; -use ui::{h_flex, prelude::*, v_flex, Avatar, Button, Icon, IconButton, IconName, Label, Tooltip}; +use ui::{ + h_flex, prelude::*, v_flex, Avatar, Button, Icon, IconButton, IconName, Label, Tab, Tooltip, +}; use util::{ResultExt, TryFutureExt}; use workspace::notifications::NotificationId; use workspace::{ @@ -588,7 +590,7 @@ impl Render for NotificationPanel { .px_2() .py_1() // Match the height of the tab bar so they line up. - .h(rems(ui::Tab::CONTAINER_HEIGHT_IN_REMS)) + .h(Tab::container_height(cx)) .border_b_1() .border_color(cx.theme().colors().border) .child(Label::new("Notifications")) diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 21dd06e81c..11bc6848fe 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -11,7 +11,7 @@ use command_palette_hooks::{ }; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global, + Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global, ParentElement, Render, Styled, Task, UpdateGlobal, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; @@ -21,9 +21,7 @@ use settings::Settings; use ui::{h_flex, prelude::*, v_flex, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing}; use util::ResultExt; use workspace::{ModalView, Workspace, WorkspaceSettings}; -use zed_actions::OpenZedUrl; - -actions!(command_palette, [Toggle]); +use zed_actions::{command_palette::Toggle, OpenZedUrl}; pub fn init(cx: &mut AppContext) { client::init_settings(cx); diff --git a/crates/context_servers/Cargo.toml b/crates/context_servers/Cargo.toml index de1e991887..cbd762c8c4 100644 --- a/crates/context_servers/Cargo.toml +++ b/crates/context_servers/Cargo.toml @@ -15,6 +15,7 @@ path = "src/context_servers.rs" anyhow.workspace = true collections.workspace = true command_palette_hooks.workspace = true +extension.workspace = true futures.workspace = true gpui.workspace = true log.workspace = true diff --git a/crates/context_servers/src/client.rs b/crates/context_servers/src/client.rs index 8202e950d6..64aabb00e8 100644 --- a/crates/context_servers/src/client.rs +++ b/crates/context_servers/src/client.rs @@ -9,7 +9,7 @@ use serde_json::{value::RawValue, Value}; use smol::{ channel, io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, - process::{self, Child}, + process::Child, }; use std::{ fmt, @@ -152,7 +152,7 @@ impl Client { &binary.args ); - let mut command = process::Command::new(&binary.executable); + let mut command = util::command::new_smol_command(&binary.executable); command .args(&binary.args) .envs(binary.env.unwrap_or_default()) diff --git a/crates/context_servers/src/context_servers.rs b/crates/context_servers/src/context_servers.rs index 87a98ca14f..e6b52aaee2 100644 --- a/crates/context_servers/src/context_servers.rs +++ b/crates/context_servers/src/context_servers.rs @@ -1,4 +1,5 @@ pub mod client; +mod extension_context_server; pub mod manager; pub mod protocol; mod registry; @@ -19,6 +20,7 @@ pub const CONTEXT_SERVERS_NAMESPACE: &'static str = "context_servers"; pub fn init(cx: &mut AppContext) { ContextServerSettings::register(cx); ContextServerFactoryRegistry::default_global(cx); + extension_context_server::init(cx); CommandPaletteFilter::update_global(cx, |filter, _cx| { filter.hide_namespace(CONTEXT_SERVERS_NAMESPACE); diff --git a/crates/context_servers/src/extension_context_server.rs b/crates/context_servers/src/extension_context_server.rs new file mode 100644 index 0000000000..092816b5e6 --- /dev/null +++ b/crates/context_servers/src/extension_context_server.rs @@ -0,0 +1,78 @@ +use std::sync::Arc; + +use extension::{Extension, ExtensionContextServerProxy, ExtensionHostProxy, ProjectDelegate}; +use gpui::{AppContext, Model}; + +use crate::manager::ServerCommand; +use crate::ContextServerFactoryRegistry; + +struct ExtensionProject { + worktree_ids: Vec, +} + +impl ProjectDelegate for ExtensionProject { + fn worktree_ids(&self) -> Vec { + self.worktree_ids.clone() + } +} + +pub fn init(cx: &mut AppContext) { + let proxy = ExtensionHostProxy::default_global(cx); + proxy.register_context_server_proxy(ContextServerFactoryRegistryProxy { + context_server_factory_registry: ContextServerFactoryRegistry::global(cx), + }); +} + +struct ContextServerFactoryRegistryProxy { + context_server_factory_registry: Model, +} + +impl ExtensionContextServerProxy for ContextServerFactoryRegistryProxy { + fn register_context_server( + &self, + extension: Arc, + id: Arc, + cx: &mut AppContext, + ) { + self.context_server_factory_registry + .update(cx, |registry, _| { + registry.register_server_factory( + id.clone(), + Arc::new({ + move |project, cx| { + log::info!( + "loading command for context server {id} from extension {}", + extension.manifest().id + ); + + let id = id.clone(); + let extension = extension.clone(); + cx.spawn(|mut cx| async move { + let extension_project = + project.update(&mut cx, |project, cx| { + Arc::new(ExtensionProject { + worktree_ids: project + .visible_worktrees(cx) + .map(|worktree| worktree.read(cx).id().to_proto()) + .collect(), + }) + })?; + + let command = extension + .context_server_command(id.clone(), extension_project) + .await?; + + log::info!("loaded command for context server {id}: {command:?}"); + + Ok(ServerCommand { + path: command.command, + args: command.args, + env: Some(command.env.into_iter().collect()), + }) + }) + } + }), + ) + }); + } +} diff --git a/crates/context_servers/src/manager.rs b/crates/context_servers/src/manager.rs index fc0c77e821..c95fcd239d 100644 --- a/crates/context_servers/src/manager.rs +++ b/crates/context_servers/src/manager.rs @@ -24,6 +24,8 @@ use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, Subscription, Tas use log; use parking_lot::RwLock; use project::Project; +use schemars::gen::SchemaGenerator; +use schemars::schema::{InstanceType, Schema, SchemaObject}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources, SettingsStore}; @@ -36,16 +38,32 @@ use crate::{ #[derive(Deserialize, Serialize, Default, Clone, PartialEq, Eq, JsonSchema, Debug)] pub struct ContextServerSettings { + /// Settings for context servers used in the Assistant. #[serde(default)] pub context_servers: HashMap, ServerConfig>, } #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug, Default)] pub struct ServerConfig { + /// The command to run this context server. + /// + /// This will override the command set by an extension. pub command: Option, + /// The settings for this context server. + /// + /// Consult the documentation for the context server to see what settings + /// are supported. + #[schemars(schema_with = "server_config_settings_json_schema")] pub settings: Option, } +fn server_config_settings_json_schema(_generator: &mut SchemaGenerator) -> Schema { + Schema::Object(SchemaObject { + instance_type: Some(InstanceType::Object.into()), + ..Default::default() + }) +} + #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)] pub struct ServerCommand { pub path: String, diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 2a54497562..2cbe76c16e 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -29,14 +29,14 @@ anyhow.workspace = true async-compression.workspace = true async-tar.workspace = true chrono.workspace = true -collections.workspace = true client.workspace = true +collections.workspace = true command_palette_hooks.workspace = true -editor.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true http_client.workspace = true +inline_completion.workspace = true language.workspace = true lsp.workspace = true menu.workspace = true @@ -44,12 +44,12 @@ node_runtime.workspace = true parking_lot.workspace = true paths.workspace = true project.workspace = true +schemars = { workspace = true, optional = true } serde.workspace = true serde_json.workspace = true -schemars = { workspace = true, optional = true } -strum.workspace = true settings.workspace = true smol.workspace = true +strum.workspace = true task.workspace = true ui.workspace = true util.workspace = true diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index a9cf406860..bc424d2d5a 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -38,8 +38,8 @@ use std::{ }; use util::{fs::remove_matching, maybe, ResultExt}; -pub use copilot_completion_provider::CopilotCompletionProvider; -pub use sign_in::CopilotCodeVerification; +pub use crate::copilot_completion_provider::CopilotCompletionProvider; +pub use crate::sign_in::{initiate_sign_in, CopilotCodeVerification}; actions!( copilot, @@ -1229,8 +1229,10 @@ mod tests { Some(self) } - fn mtime(&self) -> Option { - unimplemented!() + fn disk_state(&self) -> language::DiskState { + language::DiskState::Present { + mtime: ::fs::MTime::from_seconds_and_nanos(100, 42), + } } fn path(&self) -> &Arc { @@ -1245,10 +1247,6 @@ mod tests { unimplemented!() } - fn is_deleted(&self) -> bool { - unimplemented!() - } - fn as_any(&self) -> &dyn std::any::Any { unimplemented!() } diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 059d3a4236..85fe20f1ae 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -1,8 +1,8 @@ use crate::{Completion, Copilot}; use anyhow::Result; use client::telemetry::Telemetry; -use editor::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider}; use gpui::{AppContext, EntityId, Model, ModelContext, Task}; +use inline_completion::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider}; use language::{ language_settings::{all_language_settings, AllLanguageSettings}, Buffer, OffsetRangeExt, ToOffset, diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index d63710983b..68f0eed577 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -5,10 +5,79 @@ use gpui::{ Styled, Subscription, ViewContext, }; use ui::{prelude::*, Button, Label, Vector, VectorName}; -use workspace::ModalView; +use util::ResultExt as _; +use workspace::notifications::NotificationId; +use workspace::{ModalView, Toast, Workspace}; const COPILOT_SIGN_UP_URL: &str = "https://github.com/features/copilot"; +struct CopilotStartingToast; + +pub fn initiate_sign_in(cx: &mut WindowContext) { + let Some(copilot) = Copilot::global(cx) else { + return; + }; + let status = copilot.read(cx).status(); + let Some(workspace) = cx.window_handle().downcast::() else { + return; + }; + match status { + Status::Starting { task } => { + let Some(workspace) = cx.window_handle().downcast::() else { + return; + }; + + let Ok(workspace) = workspace.update(cx, |workspace, cx| { + workspace.show_toast( + Toast::new( + NotificationId::unique::(), + "Copilot is starting...", + ), + cx, + ); + workspace.weak_handle() + }) else { + return; + }; + + cx.spawn(|mut cx| async move { + task.await; + if let Some(copilot) = cx.update(|cx| Copilot::global(cx)).ok().flatten() { + workspace + .update(&mut cx, |workspace, cx| match copilot.read(cx).status() { + Status::Authorized => workspace.show_toast( + Toast::new( + NotificationId::unique::(), + "Copilot has started!", + ), + cx, + ), + _ => { + workspace.dismiss_toast( + &NotificationId::unique::(), + cx, + ); + copilot + .update(cx, |copilot, cx| copilot.sign_in(cx)) + .detach_and_log_err(cx); + } + }) + .log_err(); + } + }) + .detach(); + } + _ => { + copilot.update(cx, |this, cx| this.sign_in(cx)).detach(); + workspace + .update(cx, |this, cx| { + this.toggle_modal(cx, |cx| CopilotCodeVerification::new(&copilot, cx)); + }) + .ok(); + } + } +} + pub struct CopilotCodeVerification { status: Status, connect_clicked: bool, diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 933bed2711..bd0af230ab 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -727,6 +727,10 @@ impl Item for ProjectDiagnosticsEditor { self.excerpts.read(cx).is_dirty(cx) } + fn has_deleted_file(&self, cx: &AppContext) -> bool { + self.excerpts.read(cx).has_deleted_file(cx) + } + fn has_conflict(&self, cx: &AppContext) -> bool { self.excerpts.read(cx).has_conflict(cx) } @@ -772,7 +776,7 @@ impl Item for ProjectDiagnosticsEditor { } } - fn breadcrumb_location(&self) -> ToolbarItemLocation { + fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation { ToolbarItemLocation::PrimaryLeft } diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 12c1e0a50b..e3614bbd2c 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -42,10 +42,12 @@ emojis.workspace = true file_icons.workspace = true futures.workspace = true fuzzy.workspace = true +fs.workspace = true git.workspace = true gpui.workspace = true http_client.workspace = true indoc.workspace = true +inline_completion.workspace = true itertools.workspace = true language.workspace = true linkify.workspace = true diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 3b660a252f..6da9135f9e 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -271,6 +271,8 @@ gpui::actions!( Hover, Indent, JoinLines, + KillRingCut, + KillRingYank, LineDown, LineUp, MoveDown, @@ -297,6 +299,7 @@ gpui::actions!( OpenExcerptsSplit, OpenProposedChangesEditor, OpenFile, + OpenDocs, OpenPermalinkToLine, OpenUrl, Outdent, diff --git a/crates/editor/src/clangd_ext.rs b/crates/editor/src/clangd_ext.rs index 501f81b107..c018362068 100644 --- a/crates/editor/src/clangd_ext.rs +++ b/crates/editor/src/clangd_ext.rs @@ -1,5 +1,3 @@ -use std::path::PathBuf; - use anyhow::Context as _; use gpui::{View, ViewContext, WindowContext}; use language::Language; @@ -54,9 +52,9 @@ pub fn switch_source_header( cx.spawn(|_editor, mut cx| async move { let switch_source_header = switch_source_header_task .await - .with_context(|| format!("Switch source/header LSP request for path \"{}\" failed", source_file))?; + .with_context(|| format!("Switch source/header LSP request for path \"{source_file}\" failed"))?; if switch_source_header.0.is_empty() { - log::info!("Clangd returned an empty string when requesting to switch source/header from \"{}\"", source_file); + log::info!("Clangd returned an empty string when requesting to switch source/header from \"{source_file}\"" ); return Ok(()); } @@ -67,14 +65,17 @@ pub fn switch_source_header( ) })?; + let path = goto.to_file_path().map_err(|()| { + anyhow::anyhow!("URL conversion to file path failed for \"{goto}\"") + })?; + workspace .update(&mut cx, |workspace, view_cx| { - workspace.open_abs_path(PathBuf::from(goto.path()), false, view_cx) + workspace.open_abs_path(path, false, view_cx) }) .with_context(|| { format!( - "Switch source/header could not open \"{}\" in workspace", - goto.path() + "Switch source/header could not open \"{goto}\" in workspace" ) })? .await diff --git a/crates/editor/src/debounced_delay.rs b/crates/editor/src/debounced_delay.rs index 0dbf36d49e..ad4b55b209 100644 --- a/crates/editor/src/debounced_delay.rs +++ b/crates/editor/src/debounced_delay.rs @@ -5,6 +5,7 @@ use gpui::{Task, ViewContext}; use crate::Editor; +#[derive(Debug)] pub struct DebouncedDelay { task: Option>, cancel_channel: Option>, diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 7bbdc26637..3387af9113 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -66,7 +66,7 @@ use std::{ use sum_tree::{Bias, TreeMap}; use tab_map::{TabMap, TabSnapshot}; use text::LineIndent; -use ui::{div, px, IntoElement, ParentElement, SharedString, Styled, WindowContext}; +use ui::{px, SharedString, WindowContext}; use unicode_segmentation::UnicodeSegmentation; use wrap_map::{WrapMap, WrapSnapshot}; @@ -541,11 +541,17 @@ pub struct HighlightStyles { pub suggestion: Option, } +#[derive(Clone)] +pub enum ChunkReplacement { + Renderer(ChunkRenderer), + Str(SharedString), +} + pub struct HighlightedChunk<'a> { pub text: &'a str, pub style: Option, pub is_tab: bool, - pub renderer: Option, + pub replacement: Option, } impl<'a> HighlightedChunk<'a> { @@ -557,7 +563,7 @@ impl<'a> HighlightedChunk<'a> { let mut text = self.text; let style = self.style; let is_tab = self.is_tab; - let renderer = self.renderer; + let renderer = self.replacement; iter::from_fn(move || { let mut prefix_len = 0; while let Some(&ch) = chars.peek() { @@ -573,30 +579,33 @@ impl<'a> HighlightedChunk<'a> { text: prefix, style, is_tab, - renderer: renderer.clone(), + replacement: renderer.clone(), }); } chars.next(); let (prefix, suffix) = text.split_at(ch.len_utf8()); text = suffix; if let Some(replacement) = replacement(ch) { - let background = editor_style.status.hint_background; - let underline = editor_style.status.hint; + let invisible_highlight = HighlightStyle { + background_color: Some(editor_style.status.hint_background), + underline: Some(UnderlineStyle { + color: Some(editor_style.status.hint), + thickness: px(1.), + wavy: false, + }), + ..Default::default() + }; + let invisible_style = if let Some(mut style) = style { + style.highlight(invisible_highlight); + style + } else { + invisible_highlight + }; return Some(HighlightedChunk { text: prefix, - style: None, + style: Some(invisible_style), is_tab: false, - renderer: Some(ChunkRenderer { - render: Arc::new(move |_| { - div() - .child(replacement) - .bg(background) - .text_decoration_1() - .text_decoration_color(underline) - .into_any_element() - }), - constrain_width: false, - }), + replacement: Some(ChunkReplacement::Str(replacement.into())), }); } else { let invisible_highlight = HighlightStyle { @@ -619,7 +628,7 @@ impl<'a> HighlightedChunk<'a> { text: prefix, style: Some(invisible_style), is_tab: false, - renderer: renderer.clone(), + replacement: renderer.clone(), }); } } @@ -631,7 +640,7 @@ impl<'a> HighlightedChunk<'a> { text: remainder, style, is_tab, - renderer: renderer.clone(), + replacement: renderer.clone(), }) } else { None @@ -906,7 +915,7 @@ impl DisplaySnapshot { text: chunk.text, style: highlight_style, is_tab: chunk.is_tab, - renderer: chunk.renderer, + replacement: chunk.renderer.map(ChunkReplacement::Renderer), } .highlight_invisibles(editor_style) }) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 325bd138a6..9783ec0164 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -28,7 +28,6 @@ mod hover_popover; mod hunk_diff; mod indent_guides; mod inlay_hint_cache; -mod inline_completion_provider; pub mod items; mod linked_editing_ranges; mod lsp_ext; @@ -75,10 +74,10 @@ use gpui::{ div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement, AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClickEvent, ClipboardEntry, ClipboardItem, Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, - FocusOutEvent, FocusableView, FontId, FontWeight, HighlightStyle, Hsla, InteractiveText, - KeyContext, ListSizingBehavior, Model, ModelContext, MouseButton, PaintQuad, ParentElement, - Pixels, Render, ScrollStrategy, SharedString, Size, StrikethroughStyle, Styled, StyledText, - Subscription, Task, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, + FocusOutEvent, FocusableView, FontId, FontWeight, Global, HighlightStyle, Hsla, + InteractiveText, KeyContext, ListSizingBehavior, Model, ModelContext, MouseButton, PaintQuad, + ParentElement, Pixels, Render, ScrollStrategy, SharedString, Size, StrikethroughStyle, Styled, + StyledText, Subscription, Task, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext, WeakFocusHandle, WeakView, WindowContext, }; @@ -89,7 +88,8 @@ pub(crate) use hunk_diff::HoveredHunk; use hunk_diff::{diff_hunk_to_display, ExpandedHunks}; use indent_guides::ActiveIndentGuidesState; use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; -pub use inline_completion_provider::*; +pub use inline_completion::Direction; +use inline_completion::{InlayProposal, InlineCompletionProvider, InlineCompletionProviderHandle}; pub use items::MAX_TAB_TITLE_LEN; use itertools::Itertools; use language::{ @@ -277,12 +277,6 @@ enum DocumentHighlightRead {} enum DocumentHighlightWrite {} enum InputComposition {} -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum Direction { - Prev, - Next, -} - #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Navigated { Yes, @@ -545,6 +539,15 @@ pub enum IsVimMode { No, } +pub trait ActiveLineTrailerProvider { + fn render_active_line_trailer( + &mut self, + style: &EditorStyle, + focus_handle: &FocusHandle, + cx: &mut WindowContext, + ) -> Option; +} + /// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`] /// /// See the [module level documentation](self) for more information. @@ -677,6 +680,7 @@ pub struct Editor { next_scroll_position: NextScrollCursorCenterTopBottom, addons: HashMap>, _scroll_cursor_center_top_bottom_task: Task<()>, + active_line_trailer_provider: Option>, } #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] @@ -893,6 +897,7 @@ struct AutocloseRegion { struct SnippetState { ranges: Vec>>, active_index: usize, + choices: Vec>>, } #[doc(hidden)] @@ -1010,7 +1015,7 @@ enum ContextMenuOrigin { GutterIndicator(DisplayRow), } -#[derive(Clone)] +#[derive(Clone, Debug)] struct CompletionsMenu { id: CompletionId, sort_completions: bool, @@ -1021,10 +1026,105 @@ struct CompletionsMenu { matches: Arc<[StringMatch]>, selected_item: usize, scroll_handle: UniformListScrollHandle, - selected_completion_documentation_resolve_debounce: Arc>, + selected_completion_documentation_resolve_debounce: Option>>, } impl CompletionsMenu { + fn new( + id: CompletionId, + sort_completions: bool, + initial_position: Anchor, + buffer: Model, + completions: Box<[Completion]>, + ) -> Self { + let match_candidates = completions + .iter() + .enumerate() + .map(|(id, completion)| { + StringMatchCandidate::new( + id, + completion.label.text[completion.label.filter_range.clone()].into(), + ) + }) + .collect(); + + Self { + id, + sort_completions, + initial_position, + buffer, + completions: Arc::new(RwLock::new(completions)), + match_candidates, + matches: Vec::new().into(), + selected_item: 0, + scroll_handle: UniformListScrollHandle::new(), + selected_completion_documentation_resolve_debounce: Some(Arc::new(Mutex::new( + DebouncedDelay::new(), + ))), + } + } + + fn new_snippet_choices( + id: CompletionId, + sort_completions: bool, + choices: &Vec, + selection: Range, + buffer: Model, + ) -> Self { + let completions = choices + .iter() + .map(|choice| Completion { + old_range: selection.start.text_anchor..selection.end.text_anchor, + new_text: choice.to_string(), + label: CodeLabel { + text: choice.to_string(), + runs: Default::default(), + filter_range: Default::default(), + }, + server_id: LanguageServerId(usize::MAX), + documentation: None, + lsp_completion: Default::default(), + confirm: None, + }) + .collect(); + + let match_candidates = choices + .iter() + .enumerate() + .map(|(id, completion)| StringMatchCandidate::new(id, completion.to_string())) + .collect(); + let matches = choices + .iter() + .enumerate() + .map(|(id, completion)| StringMatch { + candidate_id: id, + score: 1., + positions: vec![], + string: completion.clone(), + }) + .collect(); + Self { + id, + sort_completions, + initial_position: selection.start, + buffer, + completions: Arc::new(RwLock::new(completions)), + match_candidates, + matches, + selected_item: 0, + scroll_handle: UniformListScrollHandle::new(), + selected_completion_documentation_resolve_debounce: Some(Arc::new(Mutex::new( + DebouncedDelay::new(), + ))), + } + } + + fn suppress_documentation_resolution(mut self) -> Self { + self.selected_completion_documentation_resolve_debounce + .take(); + self + } + fn select_first( &mut self, provider: Option<&dyn CompletionProvider>, @@ -1125,6 +1225,12 @@ impl CompletionsMenu { let Some(provider) = provider else { return; }; + let Some(documentation_resolve) = self + .selected_completion_documentation_resolve_debounce + .as_ref() + else { + return; + }; let resolve_task = provider.resolve_completions( self.buffer.clone(), @@ -1137,15 +1243,13 @@ impl CompletionsMenu { EditorSettings::get_global(cx).completion_documentation_secondary_query_debounce; let delay = Duration::from_millis(delay_ms); - self.selected_completion_documentation_resolve_debounce - .lock() - .fire_new(delay, cx, |_, cx| { - cx.spawn(move |this, mut cx| async move { - if let Some(true) = resolve_task.await.log_err() { - this.update(&mut cx, |_, cx| cx.notify()).ok(); - } - }) - }); + documentation_resolve.lock().fire_new(delay, cx, |_, cx| { + cx.spawn(move |this, mut cx| async move { + if let Some(true) = resolve_task.await.log_err() { + this.update(&mut cx, |_, cx| cx.notify()).ok(); + } + }) + }); } fn visible(&self) -> bool { @@ -1428,6 +1532,7 @@ impl CompletionsMenu { } } +#[derive(Clone)] struct AvailableCodeAction { excerpt_id: ExcerptId, action: CodeAction, @@ -2121,6 +2226,7 @@ impl Editor { addons: HashMap::default(), _scroll_cursor_center_top_bottom_task: Task::ready(()), text_style_refinement: None, + active_line_trailer_provider: None, }; this.tasks_update_task = Some(this.refresh_runnables(cx)); this._subscriptions.extend(project_subscriptions); @@ -2411,6 +2517,16 @@ impl Editor { self.refresh_inline_completion(false, false, cx); } + pub fn set_active_line_trailer_provider( + &mut self, + provider: Option, + _cx: &mut ViewContext, + ) where + T: ActiveLineTrailerProvider + 'static, + { + self.active_line_trailer_provider = provider.map(|provider| Box::new(provider) as Box<_>); + } + pub fn placeholder_text(&self, _cx: &WindowContext) -> Option<&str> { self.placeholder_text.as_deref() } @@ -4405,6 +4521,10 @@ impl Editor { return; }; + if !self.snippet_stack.is_empty() && self.context_menu.read().as_ref().is_some() { + return; + } + let position = self.selections.newest_anchor().head(); let (buffer, buffer_position) = if let Some(output) = self.buffer.read(cx).text_anchor_for_position(position, cx) { @@ -4450,30 +4570,13 @@ impl Editor { })?; let completions = completions.await.log_err(); let menu = if let Some(completions) = completions { - let mut menu = CompletionsMenu { + let mut menu = CompletionsMenu::new( id, sort_completions, - initial_position: position, - match_candidates: completions - .iter() - .enumerate() - .map(|(id, completion)| { - StringMatchCandidate::new( - id, - completion.label.text[completion.label.filter_range.clone()] - .into(), - ) - }) - .collect(), - buffer: buffer.clone(), - completions: Arc::new(RwLock::new(completions.into())), - matches: Vec::new().into(), - selected_item: 0, - scroll_handle: UniformListScrollHandle::new(), - selected_completion_documentation_resolve_debounce: Arc::new(Mutex::new( - DebouncedDelay::new(), - )), - }; + position, + buffer.clone(), + completions.into(), + ); menu.filter(query.as_deref(), cx.background_executor().clone()) .await; @@ -4676,7 +4779,11 @@ impl Editor { self.transact(cx, |this, cx| { if let Some(mut snippet) = snippet { snippet.text = text.to_string(); - for tabstop in snippet.tabstops.iter_mut().flatten() { + for tabstop in snippet + .tabstops + .iter_mut() + .flat_map(|tabstop| tabstop.ranges.iter_mut()) + { tabstop.start -= common_prefix_len as isize; tabstop.end -= common_prefix_len as isize; } @@ -6008,6 +6115,27 @@ impl Editor { context_menu } + fn show_snippet_choices( + &mut self, + choices: &Vec, + selection: Range, + cx: &mut ViewContext, + ) { + if selection.start.buffer_id.is_none() { + return; + } + let buffer_id = selection.start.buffer_id.unwrap(); + let buffer = self.buffer().read(cx).buffer(buffer_id); + let id = post_inc(&mut self.next_completion_id); + + if let Some(buffer) = buffer { + *self.context_menu.write() = Some(ContextMenu::Completions( + CompletionsMenu::new_snippet_choices(id, true, choices, selection, buffer) + .suppress_documentation_resolution(), + )); + } + } + pub fn insert_snippet( &mut self, insertion_ranges: &[Range], @@ -6017,6 +6145,7 @@ impl Editor { struct Tabstop { is_end_tabstop: bool, ranges: Vec>, + choices: Option>, } let tabstops = self.buffer.update(cx, |buffer, cx| { @@ -6036,10 +6165,11 @@ impl Editor { .tabstops .iter() .map(|tabstop| { - let is_end_tabstop = tabstop.first().map_or(false, |tabstop| { + let is_end_tabstop = tabstop.ranges.first().map_or(false, |tabstop| { tabstop.is_empty() && tabstop.start == snippet.text.len() as isize }); let mut tabstop_ranges = tabstop + .ranges .iter() .flat_map(|tabstop_range| { let mut delta = 0_isize; @@ -6061,6 +6191,7 @@ impl Editor { Tabstop { is_end_tabstop, ranges: tabstop_ranges, + choices: tabstop.choices.clone(), } }) .collect::>() @@ -6070,16 +6201,29 @@ impl Editor { s.select_ranges(tabstop.ranges.iter().cloned()); }); + if let Some(choices) = &tabstop.choices { + if let Some(selection) = tabstop.ranges.first() { + self.show_snippet_choices(choices, selection.clone(), cx) + } + } + // If we're already at the last tabstop and it's at the end of the snippet, // we're done, we don't need to keep the state around. if !tabstop.is_end_tabstop { + let choices = tabstops + .iter() + .map(|tabstop| tabstop.choices.clone()) + .collect(); + let ranges = tabstops .into_iter() .map(|tabstop| tabstop.ranges) .collect::>(); + self.snippet_stack.push(SnippetState { active_index: 0, ranges, + choices, }); } @@ -6154,6 +6298,13 @@ impl Editor { self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_anchor_ranges(current_ranges.iter().cloned()) }); + + if let Some(choices) = &snippet.choices[snippet.active_index] { + if let Some(selection) = current_ranges.first() { + self.show_snippet_choices(&choices, selection.clone(), cx); + } + } + // If snippet state is not at the last tabstop, push it back on the stack if snippet.active_index + 1 < snippet.ranges.len() { self.snippet_stack.push(snippet); @@ -7624,7 +7775,7 @@ impl Editor { .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); } - pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext) { + pub fn cut_common(&mut self, cx: &mut ViewContext) -> ClipboardItem { let mut text = String::new(); let buffer = self.buffer.read(cx).snapshot(cx); let mut selections = self.selections.all::(cx); @@ -7668,11 +7819,38 @@ impl Editor { s.select(selections); }); this.insert("", cx); - cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata( - text, - clipboard_selections, - )); }); + ClipboardItem::new_string_with_json_metadata(text, clipboard_selections) + } + + pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext) { + let item = self.cut_common(cx); + cx.write_to_clipboard(item); + } + + pub fn kill_ring_cut(&mut self, _: &KillRingCut, cx: &mut ViewContext) { + self.change_selections(None, cx, |s| { + s.move_with(|snapshot, sel| { + if sel.is_empty() { + sel.end = DisplayPoint::new(sel.end.row(), snapshot.line_len(sel.end.row())) + } + }); + }); + let item = self.cut_common(cx); + cx.set_global(KillRing(item)) + } + + pub fn kill_ring_yank(&mut self, _: &KillRingYank, cx: &mut ViewContext) { + let (text, metadata) = if let Some(KillRing(item)) = cx.try_global() { + if let Some(ClipboardEntry::String(kill_ring)) = item.entries().first() { + (kill_ring.text().to_string(), kill_ring.metadata_json()) + } else { + return; + } + } else { + return; + }; + self.do_paste(&text, metadata, false, cx); } pub fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { @@ -12179,6 +12357,29 @@ impl Editor { && self.has_blame_entries(cx) } + pub fn render_active_line_trailer( + &mut self, + style: &EditorStyle, + cx: &mut WindowContext, + ) -> Option { + let selection = self.selections.newest::(cx); + if !selection.is_empty() { + return None; + }; + + let snapshot = self.buffer.read(cx).snapshot(cx); + let buffer_row = MultiBufferRow(selection.head().row); + + if snapshot.line_len(buffer_row) != 0 || self.has_active_inline_completion(cx) { + return None; + } + + let focus_handle = self.focus_handle.clone(); + self.active_line_trailer_provider + .as_mut()? + .render_active_line_trailer(style, &focus_handle, cx) + } + fn has_blame_entries(&self, cx: &mut WindowContext) -> bool { self.blame() .map_or(false, |blame| blame.read(cx).has_generated_entries()) @@ -14076,7 +14277,9 @@ impl CodeActionProvider for Model { range: Range, cx: &mut WindowContext, ) -> Task>> { - self.update(cx, |project, cx| project.code_actions(buffer, range, cx)) + self.update(cx, |project, cx| { + project.code_actions(buffer, range, None, cx) + }) } fn apply_code_action( @@ -14720,15 +14923,16 @@ impl ViewInputHandler for Editor { fn text_for_range( &mut self, range_utf16: Range, + adjusted_range: &mut Option>, cx: &mut ViewContext, ) -> Option { - Some( - self.buffer - .read(cx) - .read(cx) - .text_for_range(OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end)) - .collect(), - ) + let snapshot = self.buffer.read(cx).read(cx); + let start = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.start), Bias::Left); + let end = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.end), Bias::Right); + if (start.0..end.0) != range_utf16 { + adjusted_range.replace(start.0..end.0); + } + Some(snapshot.text_for_range(start..end).collect()) } fn selected_text_range( @@ -15436,6 +15640,9 @@ fn check_multiline_range(buffer: &Buffer, range: Range) -> Range { } } +pub struct KillRing(ClipboardItem); +impl Global for KillRing {} + const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); struct BreakpointPromptEditor { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 4469b2e614..01507c4e31 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -1398,6 +1398,15 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { view.change_selections(None, cx, |s| { s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]); }); + + // moving above start of document should move selection to start of document, + // but the next move down should still be at the original goal_x + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(0, "".len())] + ); + view.move_down(&MoveDown, cx); assert_eq!( view.selections.display_ranges(cx), @@ -1422,6 +1431,25 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())] ); + // moving past end of document should not change goal_x + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(5, "".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(5, "".len())] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())] + ); + view.move_up(&MoveUp, cx); assert_eq!( view.selections.display_ranges(cx), @@ -6551,6 +6579,45 @@ async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) { }); } +#[gpui::test] +async fn test_snippet_placeholder_choices(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let (text, insertion_ranges) = marked_text_ranges( + indoc! {" + ˇ + "}, + false, + ); + + let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); + let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + + _ = editor.update(cx, |editor, cx| { + let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap(); + + editor + .insert_snippet(&insertion_ranges, snippet, cx) + .unwrap(); + + fn assert(editor: &mut Editor, cx: &mut ViewContext, marked_text: &str) { + let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false); + assert_eq!(editor.text(cx), expected_text); + assert_eq!(editor.selections.ranges::(cx), selection_ranges); + } + + assert( + editor, + cx, + indoc! {" + type «» =• + "}, + ); + + assert!(editor.context_menu_visible(), "There should be a matches"); + }); +} + #[gpui::test] async fn test_snippets(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 289aaac7c9..e1dc2b146c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -16,8 +16,8 @@ use crate::{ items::BufferSearchHighlights, mouse_context_menu::{self, MenuPosition, MouseContextMenu}, scroll::scroll_amount::ScrollAmount, - BlockId, CodeActionsMenu, CursorShape, CustomBlockId, DisplayPoint, DisplayRow, - DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings, + BlockId, ChunkReplacement, CodeActionsMenu, CursorShape, CustomBlockId, DisplayPoint, + DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, JumpData, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint, @@ -34,8 +34,8 @@ use gpui::{ FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size, - StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, TextStyleRefinement, View, - ViewContext, WeakView, WindowContext, + StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, View, ViewContext, + WeakView, WindowContext, }; use gpui::{ClickEvent, Subscription}; use itertools::Itertools; @@ -218,6 +218,8 @@ impl EditorElement { register_action(view, cx, Editor::transpose); register_action(view, cx, Editor::rewrap); register_action(view, cx, Editor::cut); + register_action(view, cx, Editor::kill_ring_cut); + register_action(view, cx, Editor::kill_ring_yank); register_action(view, cx, Editor::copy); register_action(view, cx, Editor::paste); register_action(view, cx, Editor::undo); @@ -1425,7 +1427,7 @@ impl EditorElement { } #[allow(clippy::too_many_arguments)] - fn layout_inline_blame( + fn layout_active_line_trailer( &self, display_row: DisplayRow, display_snapshot: &DisplaySnapshot, @@ -1437,61 +1439,71 @@ impl EditorElement { line_height: Pixels, cx: &mut WindowContext, ) -> Option { - if !self + let render_inline_blame = self .editor - .update(cx, |editor, cx| editor.render_git_blame_inline(cx)) - { - return None; - } + .update(cx, |editor, cx| editor.render_git_blame_inline(cx)); + if render_inline_blame { + let workspace = self + .editor + .read(cx) + .workspace + .as_ref() + .map(|(w, _)| w.clone()); - let workspace = self - .editor - .read(cx) - .workspace - .as_ref() - .map(|(w, _)| w.clone()); + let display_point = DisplayPoint::new(display_row, 0); + let buffer_row = MultiBufferRow(display_point.to_point(display_snapshot).row); - let display_point = DisplayPoint::new(display_row, 0); - let buffer_row = MultiBufferRow(display_point.to_point(display_snapshot).row); + let blame = self.editor.read(cx).blame.clone()?; + let blame_entry = blame + .update(cx, |blame, cx| { + blame.blame_for_rows([Some(buffer_row)], cx).next() + }) + .flatten()?; - let blame = self.editor.read(cx).blame.clone()?; - let blame_entry = blame - .update(cx, |blame, cx| { - blame.blame_for_rows([Some(buffer_row)], cx).next() - }) - .flatten()?; + let mut element = + render_inline_blame_entry(&blame, blame_entry, &self.style, workspace, cx); - let mut element = - render_inline_blame_entry(&blame, blame_entry, &self.style, workspace, cx); + let start_y = content_origin.y + + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height); - let start_y = content_origin.y - + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height); + let start_x = { + const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.; - let start_x = { - const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.; + let line_end = if let Some(crease_trailer) = crease_trailer { + crease_trailer.bounds.right() + } else { + content_origin.x - scroll_pixel_position.x + line_layout.width + }; + let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS; - let line_end = if let Some(crease_trailer) = crease_trailer { - crease_trailer.bounds.right() - } else { - content_origin.x - scroll_pixel_position.x + line_layout.width + let min_column_in_pixels = ProjectSettings::get_global(cx) + .git + .inline_blame + .and_then(|settings| settings.min_column) + .map(|col| self.column_pixels(col as usize, cx)) + .unwrap_or(px(0.)); + let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels; + + cmp::max(padded_line_end, min_start) }; - let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS; - let min_column_in_pixels = ProjectSettings::get_global(cx) - .git - .inline_blame - .and_then(|settings| settings.min_column) - .map(|col| self.column_pixels(col as usize, cx)) - .unwrap_or(px(0.)); - let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels; + let absolute_offset = point(start_x, start_y); + element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx); - cmp::max(padded_line_end, min_start) - }; + Some(element) + } else if let Some(mut element) = self.editor.update(cx, |editor, cx| { + editor.render_active_line_trailer(&self.style, cx) + }) { + let start_y = content_origin.y + + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height); + let start_x = content_origin.x - scroll_pixel_position.x + em_width; + let absolute_offset = point(start_x, start_y); + element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx); - let absolute_offset = point(start_x, start_y); - element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx); - - Some(element) + Some(element) + } else { + None + } } #[allow(clippy::too_many_arguments)] @@ -2103,7 +2115,7 @@ impl EditorElement { let chunks = snapshot.highlighted_chunks(rows.clone(), true, style); LineWithInvisibles::from_chunks( chunks, - &style.text, + &style, MAX_LINE_LEN, rows.len(), snapshot.mode, @@ -3541,7 +3553,7 @@ impl EditorElement { self.paint_lines(&invisible_display_ranges, layout, cx); self.paint_redactions(layout, cx); self.paint_cursors(layout, cx); - self.paint_inline_blame(layout, cx); + self.paint_active_line_trailer(layout, cx); cx.with_element_namespace("crease_trailers", |cx| { for trailer in layout.crease_trailers.iter_mut().flatten() { trailer.element.paint(cx); @@ -4023,10 +4035,10 @@ impl EditorElement { } } - fn paint_inline_blame(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { - if let Some(mut inline_blame) = layout.inline_blame.take() { + fn paint_active_line_trailer(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { + if let Some(mut element) = layout.active_line_trailer.take() { cx.paint_layer(layout.text_hitbox.bounds, |cx| { - inline_blame.paint(cx); + element.paint(cx); }) } } @@ -4460,7 +4472,7 @@ impl LineWithInvisibles { #[allow(clippy::too_many_arguments)] fn from_chunks<'a>( chunks: impl Iterator>, - text_style: &TextStyle, + editor_style: &EditorStyle, max_line_len: usize, max_line_count: usize, editor_mode: EditorMode, @@ -4468,6 +4480,7 @@ impl LineWithInvisibles { is_row_soft_wrapped: impl Copy + Fn(usize) -> bool, cx: &mut WindowContext, ) -> Vec { + let text_style = &editor_style.text; let mut layouts = Vec::with_capacity(max_line_count); let mut fragments: SmallVec<[LineFragment; 1]> = SmallVec::new(); let mut line = String::new(); @@ -4486,9 +4499,9 @@ impl LineWithInvisibles { text: "\n", style: None, is_tab: false, - renderer: None, + replacement: None, }]) { - if let Some(renderer) = highlighted_chunk.renderer { + if let Some(replacement) = highlighted_chunk.replacement { if !line.is_empty() { let shaped_line = cx .text_system() @@ -4501,42 +4514,71 @@ impl LineWithInvisibles { styles.clear(); } - let available_width = if renderer.constrain_width { - let chunk = if highlighted_chunk.text == ellipsis.as_ref() { - ellipsis.clone() - } else { - SharedString::from(Arc::from(highlighted_chunk.text)) - }; - let shaped_line = cx - .text_system() - .shape_line( - chunk, - font_size, - &[text_style.to_run(highlighted_chunk.text.len())], - ) - .unwrap(); - AvailableSpace::Definite(shaped_line.width) - } else { - AvailableSpace::MinContent - }; + match replacement { + ChunkReplacement::Renderer(renderer) => { + let available_width = if renderer.constrain_width { + let chunk = if highlighted_chunk.text == ellipsis.as_ref() { + ellipsis.clone() + } else { + SharedString::from(Arc::from(highlighted_chunk.text)) + }; + let shaped_line = cx + .text_system() + .shape_line( + chunk, + font_size, + &[text_style.to_run(highlighted_chunk.text.len())], + ) + .unwrap(); + AvailableSpace::Definite(shaped_line.width) + } else { + AvailableSpace::MinContent + }; - let mut element = (renderer.render)(&mut ChunkRendererContext { - context: cx, - max_width: text_width, - }); - let line_height = text_style.line_height_in_pixels(cx.rem_size()); - let size = element.layout_as_root( - size(available_width, AvailableSpace::Definite(line_height)), - cx, - ); + let mut element = (renderer.render)(&mut ChunkRendererContext { + context: cx, + max_width: text_width, + }); + let line_height = text_style.line_height_in_pixels(cx.rem_size()); + let size = element.layout_as_root( + size(available_width, AvailableSpace::Definite(line_height)), + cx, + ); - width += size.width; - len += highlighted_chunk.text.len(); - fragments.push(LineFragment::Element { - element: Some(element), - size, - len: highlighted_chunk.text.len(), - }); + width += size.width; + len += highlighted_chunk.text.len(); + fragments.push(LineFragment::Element { + element: Some(element), + size, + len: highlighted_chunk.text.len(), + }); + } + ChunkReplacement::Str(x) => { + let text_style = if let Some(style) = highlighted_chunk.style { + Cow::Owned(text_style.clone().highlight(style)) + } else { + Cow::Borrowed(text_style) + }; + + let run = TextRun { + len: x.len(), + font: text_style.font(), + color: text_style.color, + background_color: text_style.background_color, + underline: text_style.underline, + strikethrough: text_style.strikethrough, + }; + let line_layout = cx + .text_system() + .shape_line(x, font_size, &[run]) + .unwrap() + .with_len(highlighted_chunk.text.len()); + + width += line_layout.width; + len += highlighted_chunk.text.len(); + fragments.push(LineFragment::Text(line_layout)) + } + } } else { for (ix, mut line_chunk) in highlighted_chunk.text.split('\n').enumerate() { if ix > 0 { @@ -5413,14 +5455,14 @@ impl Element for EditorElement { ) }); - let mut inline_blame = None; + let mut active_line_trailer = None; if let Some(newest_selection_head) = newest_selection_head { let display_row = newest_selection_head.row(); if (start_row..end_row).contains(&display_row) { let line_ix = display_row.minus(start_row) as usize; let line_layout = &line_layouts[line_ix]; let crease_trailer_layout = crease_trailers[line_ix].as_ref(); - inline_blame = self.layout_inline_blame( + active_line_trailer = self.layout_active_line_trailer( display_row, &snapshot.display_snapshot, line_layout, @@ -5753,7 +5795,7 @@ impl Element for EditorElement { line_elements, line_numbers, blamed_display_rows, - inline_blame, + active_line_trailer, blocks, cursors, visible_cursors, @@ -5891,7 +5933,7 @@ pub struct EditorLayout { line_numbers: Vec>, display_hunks: Vec<(DisplayDiffHunk, Option)>, blamed_display_rows: Option>, - inline_blame: Option, + active_line_trailer: Option, blocks: Vec, highlighted_ranges: Vec<(Range, Hsla)>, highlighted_gutter_ranges: Vec<(Range, Hsla)>, @@ -6120,7 +6162,7 @@ fn layout_line( let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style); LineWithInvisibles::from_chunks( chunks, - &style.text, + &style, MAX_LINE_LEN, 1, snapshot.mode, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index bc8119c829..51ad9b9dec 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -16,7 +16,8 @@ use gpui::{ VisualContext, WeakView, WindowContext, }; use language::{ - proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, Point, SelectionGoal, + proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, DiskState, Point, + SelectionGoal, }; use lsp::DiagnosticSeverity; use multi_buffer::AnchorRangeExt; @@ -635,12 +636,13 @@ impl Item for Editor { Some(util::truncate_and_trailoff(description, MAX_TAB_TITLE_LEN)) }); - let is_deleted: bool = self + // Whether the file was saved in the past but is now deleted. + let was_deleted: bool = self .buffer() .read(cx) .as_singleton() .and_then(|buffer| buffer.read(cx).file()) - .map_or(true, |file| file.is_deleted()); + .map_or(false, |file| file.disk_state() == DiskState::Deleted); h_flex() .gap_2() @@ -648,7 +650,7 @@ impl Item for Editor { Label::new(self.title(cx).to_string()) .color(label_color) .italic(params.preview) - .strikethrough(is_deleted), + .strikethrough(was_deleted), ) .when_some(description, |this, description| { this.child( @@ -708,6 +710,10 @@ impl Item for Editor { self.buffer().read(cx).read(cx).is_dirty() } + fn has_deleted_file(&self, cx: &AppContext) -> bool { + self.buffer().read(cx).read(cx).has_deleted_file() + } + fn has_conflict(&self, cx: &AppContext) -> bool { self.buffer().read(cx).read(cx).has_conflict() } @@ -835,7 +841,7 @@ impl Item for Editor { self.pixel_position_of_newest_cursor } - fn breadcrumb_location(&self) -> ToolbarItemLocation { + fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation { if self.show_breadcrumbs { ToolbarItemLocation::PrimaryLeft } else { @@ -1612,15 +1618,14 @@ fn path_for_file<'a>( #[cfg(test)] mod tests { use crate::editor_tests::init_test; + use fs::Fs; use super::*; + use fs::MTime; use gpui::{AppContext, VisualTestContext}; use language::{LanguageMatcher, TestFile}; use project::FakeFs; - use std::{ - path::{Path, PathBuf}, - time::SystemTime, - }; + use std::path::{Path, PathBuf}; #[gpui::test] fn test_path_for_file(cx: &mut AppContext) { @@ -1673,9 +1678,7 @@ mod tests { async fn test_deserialize(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); - let now = SystemTime::now(); let fs = FakeFs::new(cx.executor()); - fs.set_next_mtime(now); fs.insert_file("/file.rs", Default::default()).await; // Test case 1: Deserialize with path and contents @@ -1684,12 +1687,18 @@ mod tests { let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap(); let item_id = 1234 as ItemId; + let mtime = fs + .metadata(Path::new("/file.rs")) + .await + .unwrap() + .unwrap() + .mtime; let serialized_editor = SerializedEditor { abs_path: Some(PathBuf::from("/file.rs")), contents: Some("fn main() {}".to_string()), language: Some("Rust".to_string()), - mtime: Some(now), + mtime: Some(mtime), }; DB.save_serialized_editor(item_id, workspace_id, serialized_editor.clone()) @@ -1786,9 +1795,7 @@ mod tests { let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap(); let item_id = 9345 as ItemId; - let old_mtime = now - .checked_sub(std::time::Duration::from_secs(60 * 60 * 24)) - .unwrap(); + let old_mtime = MTime::from_seconds_and_nanos(0, 50); let serialized_editor = SerializedEditor { abs_path: Some(PathBuf::from("/file.rs")), contents: Some("fn main() {}".to_string()), diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 19ba147e16..52bedde2e3 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -3,7 +3,7 @@ use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; use crate::{scroll::ScrollAnchor, CharKind, DisplayRow, EditorStyle, RowExt, ToOffset, ToPoint}; -use gpui::{px, Pixels, WindowTextSystem}; +use gpui::{Pixels, WindowTextSystem}; use language::Point; use multi_buffer::{MultiBufferRow, MultiBufferSnapshot}; use serde::Deserialize; @@ -120,7 +120,7 @@ pub(crate) fn up_by_rows( preserve_column_at_start: bool, text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { - let mut goal_x = match goal { + let goal_x = match goal { SelectionGoal::HorizontalPosition(x) => x.into(), SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(), SelectionGoal::HorizontalRange { end, .. } => end.into(), @@ -138,7 +138,6 @@ pub(crate) fn up_by_rows( return (start, goal); } else { point = DisplayPoint::new(DisplayRow(0), 0); - goal_x = px(0.); } let mut clipped_point = map.clip_point(point, Bias::Left); @@ -159,7 +158,7 @@ pub(crate) fn down_by_rows( preserve_column_at_end: bool, text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { - let mut goal_x = match goal { + let goal_x = match goal { SelectionGoal::HorizontalPosition(x) => x.into(), SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(), SelectionGoal::HorizontalRange { end, .. } => end.into(), @@ -174,7 +173,6 @@ pub(crate) fn down_by_rows( return (start, goal); } else { point = map.max_point(); - goal_x = map.x_for_display_point(point, text_layout_details) } let mut clipped_point = map.clip_point(point, Bias::Right); @@ -610,7 +608,7 @@ mod tests { test::{editor_test_context::EditorTestContext, marked_display_snapshot}, Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, InlayId, MultiBuffer, }; - use gpui::{font, Context as _}; + use gpui::{font, px, Context as _}; use language::Capability; use project::Project; use settings::SettingsStore; @@ -977,7 +975,7 @@ mod tests { ), ( DisplayPoint::new(DisplayRow(2), 0), - SelectionGoal::HorizontalPosition(0.0) + SelectionGoal::HorizontalPosition(col_2_x.0), ), ); assert_eq!( @@ -990,7 +988,7 @@ mod tests { ), ( DisplayPoint::new(DisplayRow(2), 0), - SelectionGoal::HorizontalPosition(0.0) + SelectionGoal::HorizontalPosition(0.0), ), ); @@ -1059,7 +1057,7 @@ mod tests { let max_point_x = snapshot .x_for_display_point(DisplayPoint::new(DisplayRow(7), 2), &text_layout_details); - // Can't move down off the end + // Can't move down off the end, and attempting to do so leaves the selection goal unchanged assert_eq!( down( &snapshot, @@ -1070,7 +1068,7 @@ mod tests { ), ( DisplayPoint::new(DisplayRow(7), 2), - SelectionGoal::HorizontalPosition(max_point_x.0) + SelectionGoal::HorizontalPosition(0.0) ), ); assert_eq!( diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs index a52fb60543..06e2ea1f9b 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -1,8 +1,8 @@ use anyhow::Result; use db::sqlez::bindable::{Bind, Column, StaticColumnCount}; use db::sqlez::statement::Statement; +use fs::MTime; use std::path::PathBuf; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; use db::sqlez_macros::sql; use db::{define_connection, query}; @@ -14,7 +14,7 @@ pub(crate) struct SerializedEditor { pub(crate) abs_path: Option, pub(crate) contents: Option, pub(crate) language: Option, - pub(crate) mtime: Option, + pub(crate) mtime: Option, } impl StaticColumnCount for SerializedEditor { @@ -29,16 +29,13 @@ impl Bind for SerializedEditor { let start_index = statement.bind(&self.contents, start_index)?; let start_index = statement.bind(&self.language, start_index)?; - let mtime = self.mtime.and_then(|mtime| { - mtime - .duration_since(UNIX_EPOCH) - .ok() - .map(|duration| (duration.as_secs() as i64, duration.subsec_nanos() as i32)) - }); - let start_index = match mtime { + let start_index = match self + .mtime + .and_then(|mtime| mtime.to_seconds_and_nanos_for_persistence()) + { Some((seconds, nanos)) => { - let start_index = statement.bind(&seconds, start_index)?; - statement.bind(&nanos, start_index)? + let start_index = statement.bind(&(seconds as i64), start_index)?; + statement.bind(&(nanos as i32), start_index)? } None => { let start_index = statement.bind::>(&None, start_index)?; @@ -64,7 +61,7 @@ impl Column for SerializedEditor { let mtime = mtime_seconds .zip(mtime_nanos) - .map(|(seconds, nanos)| UNIX_EPOCH + Duration::new(seconds as u64, nanos as u32)); + .map(|(seconds, nanos)| MTime::from_seconds_and_nanos(seconds as u64, nanos as u32)); let editor = Self { abs_path, @@ -280,12 +277,11 @@ mod tests { assert_eq!(have, serialized_editor); // Storing and retrieving mtime - let now = SystemTime::now(); let serialized_editor = SerializedEditor { abs_path: None, contents: None, language: None, - mtime: Some(now), + mtime: Some(MTime::from_seconds_and_nanos(100, 42)), }; DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone()) diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index fa39e5c9d4..ba14f91ed2 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -1,3 +1,5 @@ +use std::{fs, path::Path}; + use anyhow::Context as _; use gpui::{Context, View, ViewContext, VisualContext, WindowContext}; use language::Language; @@ -7,7 +9,7 @@ use text::ToPointUtf16; use crate::{ element::register_action, lsp_ext::find_specific_language_server_in_selection, Editor, - ExpandMacroRecursively, + ExpandMacroRecursively, OpenDocs, }; const RUST_ANALYZER_NAME: &str = "rust-analyzer"; @@ -24,6 +26,7 @@ pub fn apply_related_actions(editor: &View, cx: &mut WindowContext) { .is_some() { register_action(editor, cx, expand_macro_recursively); + register_action(editor, cx, open_docs); } } @@ -94,3 +97,64 @@ pub fn expand_macro_recursively( }) .detach_and_log_err(cx); } + +pub fn open_docs(editor: &mut Editor, _: &OpenDocs, cx: &mut ViewContext<'_, Editor>) { + if editor.selections.count() == 0 { + return; + } + let Some(project) = &editor.project else { + return; + }; + let Some(workspace) = editor.workspace() else { + return; + }; + + let Some((trigger_anchor, _rust_language, server_to_query, buffer)) = + find_specific_language_server_in_selection( + editor, + cx, + is_rust_language, + RUST_ANALYZER_NAME, + ) + else { + return; + }; + + let project = project.clone(); + let buffer_snapshot = buffer.read(cx).snapshot(); + let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot); + let open_docs_task = project.update(cx, |project, cx| { + project.request_lsp( + buffer, + project::LanguageServerToQuery::Other(server_to_query), + project::lsp_ext_command::OpenDocs { position }, + cx, + ) + }); + + cx.spawn(|_editor, mut cx| async move { + let docs_urls = open_docs_task.await.context("open docs")?; + if docs_urls.is_empty() { + log::debug!("Empty docs urls for position {position:?}"); + return Ok(()); + } else { + log::debug!("{:?}", docs_urls); + } + + workspace.update(&mut cx, |_workspace, cx| { + // Check if the local document exists, otherwise fallback to the online document. + // Open with the default browser. + if let Some(local_url) = docs_urls.local { + if fs::metadata(Path::new(&local_url[8..])).is_ok() { + cx.open_url(&local_url); + return; + } + } + + if let Some(web_url) = docs_urls.web { + cx.open_url(&web_url); + } + }) + }) + .detach_and_log_err(cx); +} diff --git a/crates/evals/Cargo.toml b/crates/evals/Cargo.toml index 3057edcd1a..744094aeaf 100644 --- a/crates/evals/Cargo.toml +++ b/crates/evals/Cargo.toml @@ -30,9 +30,10 @@ languages.workspace = true node_runtime.workspace = true open_ai.workspace = true project.workspace = true +reqwest_client.workspace = true semantic_index.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true smol.workspace = true -reqwest_client.workspace = true +util.workspace = true diff --git a/crates/evals/src/eval.rs b/crates/evals/src/eval.rs index 2db13ff392..67b73a835b 100644 --- a/crates/evals/src/eval.rs +++ b/crates/evals/src/eval.rs @@ -27,7 +27,7 @@ use std::time::Duration; use std::{ fs, path::Path, - process::{exit, Command, Stdio}, + process::{exit, Stdio}, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, @@ -667,7 +667,7 @@ async fn fetch_eval_repo( return; } if !repo_dir.join(".git").exists() { - let init_output = Command::new("git") + let init_output = util::command::new_std_command("git") .current_dir(&repo_dir) .args(&["init"]) .output() @@ -682,13 +682,13 @@ async fn fetch_eval_repo( } } let url = format!("https://github.com/{}.git", repo); - Command::new("git") + util::command::new_std_command("git") .current_dir(&repo_dir) .args(&["remote", "add", "-f", "origin", &url]) .stdin(Stdio::null()) .output() .unwrap(); - let fetch_output = Command::new("git") + let fetch_output = util::command::new_std_command("git") .current_dir(&repo_dir) .args(&["fetch", "--depth", "1", "origin", &sha]) .stdin(Stdio::null()) @@ -703,7 +703,7 @@ async fn fetch_eval_repo( ); return; } - let checkout_output = Command::new("git") + let checkout_output = util::command::new_std_command("git") .current_dir(&repo_dir) .args(&["checkout", &sha]) .output() diff --git a/crates/extension/Cargo.toml b/crates/extension/Cargo.toml index b4d23fd709..b92771d09d 100644 --- a/crates/extension/Cargo.toml +++ b/crates/extension/Cargo.toml @@ -24,10 +24,12 @@ http_client.workspace = true language.workspace = true log.workspace = true lsp.workspace = true +parking_lot.workspace = true semantic_version.workspace = true serde.workspace = true serde_json.workspace = true toml.workspace = true +util.workspace = true wasm-encoder.workspace = true wasmparser.workspace = true wit-component.workspace = true diff --git a/crates/extension/src/extension.rs b/crates/extension/src/extension.rs index a3c275c537..2eb067ca40 100644 --- a/crates/extension/src/extension.rs +++ b/crates/extension/src/extension.rs @@ -1,4 +1,5 @@ pub mod extension_builder; +mod extension_host_proxy; mod extension_manifest; mod types; @@ -9,13 +10,19 @@ use ::lsp::LanguageServerName; use anyhow::{anyhow, bail, Context as _, Result}; use async_trait::async_trait; use fs::normalize_path; -use gpui::Task; +use gpui::{AppContext, Task}; use language::LanguageName; use semantic_version::SemanticVersion; +pub use crate::extension_host_proxy::*; pub use crate::extension_manifest::*; pub use crate::types::*; +/// Initializes the `extension` crate. +pub fn init(cx: &mut AppContext) { + ExtensionHostProxy::default_global(cx); +} + #[async_trait] pub trait WorktreeDelegate: Send + Sync + 'static { fn id(&self) -> u64; @@ -25,6 +32,10 @@ pub trait WorktreeDelegate: Send + Sync + 'static { async fn shell_env(&self) -> Vec<(String, String)>; } +pub trait ProjectDelegate: Send + Sync + 'static { + fn worktree_ids(&self) -> Vec; +} + pub trait KeyValueStoreDelegate: Send + Sync + 'static { fn insert(&self, key: String, docs: String) -> Task>; } @@ -87,6 +98,12 @@ pub trait Extension: Send + Sync + 'static { worktree: Option>, ) -> Result; + async fn context_server_command( + &self, + context_server_id: Arc, + project: Arc, + ) -> Result; + async fn suggest_docs_packages(&self, provider: Arc) -> Result>; async fn index_docs( diff --git a/crates/extension/src/extension_builder.rs b/crates/extension/src/extension_builder.rs index 25e6a1a485..a2d7ae573f 100644 --- a/crates/extension/src/extension_builder.rs +++ b/crates/extension/src/extension_builder.rs @@ -11,7 +11,7 @@ use serde::Deserialize; use std::{ env, fs, mem, path::{Path, PathBuf}, - process::{Command, Stdio}, + process::Stdio, sync::Arc, }; use wasm_encoder::{ComponentSectionId, Encode as _, RawSection, Section as _}; @@ -130,7 +130,7 @@ impl ExtensionBuilder { "compiling Rust crate for extension {}", extension_dir.display() ); - let output = Command::new("cargo") + let output = util::command::new_std_command("cargo") .args(["build", "--target", RUST_TARGET]) .args(options.release.then_some("--release")) .arg("--target-dir") @@ -237,7 +237,7 @@ impl ExtensionBuilder { let scanner_path = src_path.join("scanner.c"); log::info!("compiling {grammar_name} parser"); - let clang_output = Command::new(&clang_path) + let clang_output = util::command::new_std_command(&clang_path) .args(["-fPIC", "-shared", "-Os"]) .arg(format!("-Wl,--export=tree_sitter_{grammar_name}")) .arg("-o") @@ -264,7 +264,7 @@ impl ExtensionBuilder { let git_dir = directory.join(".git"); if directory.exists() { - let remotes_output = Command::new("git") + let remotes_output = util::command::new_std_command("git") .arg("--git-dir") .arg(&git_dir) .args(["remote", "-v"]) @@ -287,7 +287,7 @@ impl ExtensionBuilder { fs::create_dir_all(directory).with_context(|| { format!("failed to create grammar directory {}", directory.display(),) })?; - let init_output = Command::new("git") + let init_output = util::command::new_std_command("git") .arg("init") .current_dir(directory) .output()?; @@ -298,7 +298,7 @@ impl ExtensionBuilder { ); } - let remote_add_output = Command::new("git") + let remote_add_output = util::command::new_std_command("git") .arg("--git-dir") .arg(&git_dir) .args(["remote", "add", "origin", url]) @@ -312,14 +312,14 @@ impl ExtensionBuilder { } } - let fetch_output = Command::new("git") + let fetch_output = util::command::new_std_command("git") .arg("--git-dir") .arg(&git_dir) .args(["fetch", "--depth", "1", "origin", rev]) .output() .context("failed to execute `git fetch`")?; - let checkout_output = Command::new("git") + let checkout_output = util::command::new_std_command("git") .arg("--git-dir") .arg(&git_dir) .args(["checkout", rev]) @@ -346,7 +346,7 @@ impl ExtensionBuilder { } fn install_rust_wasm_target_if_needed(&self) -> Result<()> { - let rustc_output = Command::new("rustc") + let rustc_output = util::command::new_std_command("rustc") .arg("--print") .arg("sysroot") .output() @@ -363,7 +363,7 @@ impl ExtensionBuilder { return Ok(()); } - let output = Command::new("rustup") + let output = util::command::new_std_command("rustup") .args(["target", "add", RUST_TARGET]) .stderr(Stdio::piped()) .stdout(Stdio::inherit()) diff --git a/crates/extension/src/extension_host_proxy.rs b/crates/extension/src/extension_host_proxy.rs new file mode 100644 index 0000000000..8909a6082d --- /dev/null +++ b/crates/extension/src/extension_host_proxy.rs @@ -0,0 +1,324 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::Result; +use fs::Fs; +use gpui::{AppContext, Global, ReadGlobal, SharedString, Task}; +use language::{LanguageMatcher, LanguageName, LanguageServerBinaryStatus, LoadedLanguage}; +use lsp::LanguageServerName; +use parking_lot::RwLock; + +use crate::{Extension, SlashCommand}; + +#[derive(Default)] +struct GlobalExtensionHostProxy(Arc); + +impl Global for GlobalExtensionHostProxy {} + +/// A proxy for interacting with the extension host. +/// +/// This object implements each of the individual proxy types so that their +/// methods can be called directly on it. +#[derive(Default)] +pub struct ExtensionHostProxy { + theme_proxy: RwLock>>, + grammar_proxy: RwLock>>, + language_proxy: RwLock>>, + language_server_proxy: RwLock>>, + snippet_proxy: RwLock>>, + slash_command_proxy: RwLock>>, + context_server_proxy: RwLock>>, + indexed_docs_provider_proxy: RwLock>>, +} + +impl ExtensionHostProxy { + /// Returns the global [`ExtensionHostProxy`]. + pub fn global(cx: &AppContext) -> Arc { + GlobalExtensionHostProxy::global(cx).0.clone() + } + + /// Returns the global [`ExtensionHostProxy`]. + /// + /// Inserts a default [`ExtensionHostProxy`] if one does not yet exist. + pub fn default_global(cx: &mut AppContext) -> Arc { + cx.default_global::().0.clone() + } + + pub fn new() -> Self { + Self { + theme_proxy: RwLock::default(), + grammar_proxy: RwLock::default(), + language_proxy: RwLock::default(), + language_server_proxy: RwLock::default(), + snippet_proxy: RwLock::default(), + slash_command_proxy: RwLock::default(), + context_server_proxy: RwLock::default(), + indexed_docs_provider_proxy: RwLock::default(), + } + } + + pub fn register_theme_proxy(&self, proxy: impl ExtensionThemeProxy) { + self.theme_proxy.write().replace(Arc::new(proxy)); + } + + pub fn register_grammar_proxy(&self, proxy: impl ExtensionGrammarProxy) { + self.grammar_proxy.write().replace(Arc::new(proxy)); + } + + pub fn register_language_proxy(&self, proxy: impl ExtensionLanguageProxy) { + self.language_proxy.write().replace(Arc::new(proxy)); + } + + pub fn register_language_server_proxy(&self, proxy: impl ExtensionLanguageServerProxy) { + self.language_server_proxy.write().replace(Arc::new(proxy)); + } + + pub fn register_snippet_proxy(&self, proxy: impl ExtensionSnippetProxy) { + self.snippet_proxy.write().replace(Arc::new(proxy)); + } + + pub fn register_slash_command_proxy(&self, proxy: impl ExtensionSlashCommandProxy) { + self.slash_command_proxy.write().replace(Arc::new(proxy)); + } + + pub fn register_context_server_proxy(&self, proxy: impl ExtensionContextServerProxy) { + self.context_server_proxy.write().replace(Arc::new(proxy)); + } + + pub fn register_indexed_docs_provider_proxy( + &self, + proxy: impl ExtensionIndexedDocsProviderProxy, + ) { + self.indexed_docs_provider_proxy + .write() + .replace(Arc::new(proxy)); + } +} + +pub trait ExtensionThemeProxy: Send + Sync + 'static { + fn list_theme_names(&self, theme_path: PathBuf, fs: Arc) -> Task>>; + + fn remove_user_themes(&self, themes: Vec); + + fn load_user_theme(&self, theme_path: PathBuf, fs: Arc) -> Task>; + + fn reload_current_theme(&self, cx: &mut AppContext); +} + +impl ExtensionThemeProxy for ExtensionHostProxy { + fn list_theme_names(&self, theme_path: PathBuf, fs: Arc) -> Task>> { + let Some(proxy) = self.theme_proxy.read().clone() else { + return Task::ready(Ok(Vec::new())); + }; + + proxy.list_theme_names(theme_path, fs) + } + + fn remove_user_themes(&self, themes: Vec) { + let Some(proxy) = self.theme_proxy.read().clone() else { + return; + }; + + proxy.remove_user_themes(themes) + } + + fn load_user_theme(&self, theme_path: PathBuf, fs: Arc) -> Task> { + let Some(proxy) = self.theme_proxy.read().clone() else { + return Task::ready(Ok(())); + }; + + proxy.load_user_theme(theme_path, fs) + } + + fn reload_current_theme(&self, cx: &mut AppContext) { + let Some(proxy) = self.theme_proxy.read().clone() else { + return; + }; + + proxy.reload_current_theme(cx) + } +} + +pub trait ExtensionGrammarProxy: Send + Sync + 'static { + fn register_grammars(&self, grammars: Vec<(Arc, PathBuf)>); +} + +impl ExtensionGrammarProxy for ExtensionHostProxy { + fn register_grammars(&self, grammars: Vec<(Arc, PathBuf)>) { + let Some(proxy) = self.grammar_proxy.read().clone() else { + return; + }; + + proxy.register_grammars(grammars) + } +} + +pub trait ExtensionLanguageProxy: Send + Sync + 'static { + fn register_language( + &self, + language: LanguageName, + grammar: Option>, + matcher: LanguageMatcher, + load: Arc Result + Send + Sync + 'static>, + ); + + fn remove_languages( + &self, + languages_to_remove: &[LanguageName], + grammars_to_remove: &[Arc], + ); +} + +impl ExtensionLanguageProxy for ExtensionHostProxy { + fn register_language( + &self, + language: LanguageName, + grammar: Option>, + matcher: LanguageMatcher, + load: Arc Result + Send + Sync + 'static>, + ) { + let Some(proxy) = self.language_proxy.read().clone() else { + return; + }; + + proxy.register_language(language, grammar, matcher, load) + } + + fn remove_languages( + &self, + languages_to_remove: &[LanguageName], + grammars_to_remove: &[Arc], + ) { + let Some(proxy) = self.language_proxy.read().clone() else { + return; + }; + + proxy.remove_languages(languages_to_remove, grammars_to_remove) + } +} + +pub trait ExtensionLanguageServerProxy: Send + Sync + 'static { + fn register_language_server( + &self, + extension: Arc, + language_server_id: LanguageServerName, + language: LanguageName, + ); + + fn remove_language_server( + &self, + language: &LanguageName, + language_server_id: &LanguageServerName, + ); + + fn update_language_server_status( + &self, + language_server_id: LanguageServerName, + status: LanguageServerBinaryStatus, + ); +} + +impl ExtensionLanguageServerProxy for ExtensionHostProxy { + fn register_language_server( + &self, + extension: Arc, + language_server_id: LanguageServerName, + language: LanguageName, + ) { + let Some(proxy) = self.language_server_proxy.read().clone() else { + return; + }; + + proxy.register_language_server(extension, language_server_id, language) + } + + fn remove_language_server( + &self, + language: &LanguageName, + language_server_id: &LanguageServerName, + ) { + let Some(proxy) = self.language_server_proxy.read().clone() else { + return; + }; + + proxy.remove_language_server(language, language_server_id) + } + + fn update_language_server_status( + &self, + language_server_id: LanguageServerName, + status: LanguageServerBinaryStatus, + ) { + let Some(proxy) = self.language_server_proxy.read().clone() else { + return; + }; + + proxy.update_language_server_status(language_server_id, status) + } +} + +pub trait ExtensionSnippetProxy: Send + Sync + 'static { + fn register_snippet(&self, path: &PathBuf, snippet_contents: &str) -> Result<()>; +} + +impl ExtensionSnippetProxy for ExtensionHostProxy { + fn register_snippet(&self, path: &PathBuf, snippet_contents: &str) -> Result<()> { + let Some(proxy) = self.snippet_proxy.read().clone() else { + return Ok(()); + }; + + proxy.register_snippet(path, snippet_contents) + } +} + +pub trait ExtensionSlashCommandProxy: Send + Sync + 'static { + fn register_slash_command(&self, extension: Arc, command: SlashCommand); +} + +impl ExtensionSlashCommandProxy for ExtensionHostProxy { + fn register_slash_command(&self, extension: Arc, command: SlashCommand) { + let Some(proxy) = self.slash_command_proxy.read().clone() else { + return; + }; + + proxy.register_slash_command(extension, command) + } +} + +pub trait ExtensionContextServerProxy: Send + Sync + 'static { + fn register_context_server( + &self, + extension: Arc, + server_id: Arc, + cx: &mut AppContext, + ); +} + +impl ExtensionContextServerProxy for ExtensionHostProxy { + fn register_context_server( + &self, + extension: Arc, + server_id: Arc, + cx: &mut AppContext, + ) { + let Some(proxy) = self.context_server_proxy.read().clone() else { + return; + }; + + proxy.register_context_server(extension, server_id, cx) + } +} + +pub trait ExtensionIndexedDocsProviderProxy: Send + Sync + 'static { + fn register_indexed_docs_provider(&self, extension: Arc, provider_id: Arc); +} + +impl ExtensionIndexedDocsProviderProxy for ExtensionHostProxy { + fn register_indexed_docs_provider(&self, extension: Arc, provider_id: Arc) { + let Some(proxy) = self.indexed_docs_provider_proxy.read().clone() else { + return; + }; + + proxy.register_indexed_docs_provider(extension, provider_id) + } +} diff --git a/crates/extension/src/types.rs b/crates/extension/src/types.rs index f4c37b5daf..f04d31300f 100644 --- a/crates/extension/src/types.rs +++ b/crates/extension/src/types.rs @@ -10,6 +10,7 @@ pub use slash_command::*; pub type EnvVars = Vec<(String, String)>; /// A command. +#[derive(Debug)] pub struct Command { /// The command to execute. pub command: String, diff --git a/crates/extension_host/Cargo.toml b/crates/extension_host/Cargo.toml index 856466e1a1..6e78654b7e 100644 --- a/crates/extension_host/Cargo.toml +++ b/crates/extension_host/Cargo.toml @@ -34,6 +34,7 @@ lsp.workspace = true node_runtime.workspace = true paths.workspace = true project.workspace = true +remote.workspace = true release_channel.workspace = true schemars.workspace = true semantic_version.workspace = true @@ -42,6 +43,7 @@ serde_json.workspace = true serde_json_lenient.workspace = true settings.workspace = true task.workspace = true +tempfile.workspace = true toml.workspace = true url.workspace = true util.workspace = true @@ -55,7 +57,9 @@ env_logger.workspace = true fs = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] } +language_extension.workspace = true parking_lot.workspace = true project = { workspace = true, features = ["test-support"] } reqwest_client.workspace = true theme = { workspace = true, features = ["test-support"] } +theme_extension.workspace = true diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 1adea4e0fb..aab5c258f5 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1,19 +1,22 @@ -pub mod extension_lsp_adapter; pub mod extension_settings; +pub mod headless_host; pub mod wasm_host; #[cfg(test)] mod extension_store_test; -use crate::extension_lsp_adapter::ExtensionLspAdapter; use anyhow::{anyhow, bail, Context as _, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; -use client::{telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse}; -use collections::{btree_map, BTreeMap, HashSet}; +use client::{proto, telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse}; +use collections::{btree_map, BTreeMap, HashMap, HashSet}; use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; -use extension::Extension; pub use extension::ExtensionManifest; +use extension::{ + ExtensionContextServerProxy, ExtensionGrammarProxy, ExtensionHostProxy, + ExtensionIndexedDocsProviderProxy, ExtensionLanguageProxy, ExtensionLanguageServerProxy, + ExtensionSlashCommandProxy, ExtensionSnippetProxy, ExtensionThemeProxy, +}; use fs::{Fs, RemoveOptions}; use futures::{ channel::{ @@ -24,18 +27,18 @@ use futures::{ select_biased, AsyncReadExt as _, Future, FutureExt as _, StreamExt as _, }; use gpui::{ - actions, AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, - SharedString, Task, WeakModel, + actions, AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Task, + WeakModel, }; use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; use language::{ LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LoadedLanguage, QUERY_FILENAME_PREFIXES, }; -use lsp::LanguageServerName; use node_runtime::NodeRuntime; use project::ContextProviderWithTasks; use release_channel::ReleaseChannel; +use remote::SshRemoteClient; use semantic_version::SemanticVersion; use serde::{Deserialize, Serialize}; use settings::Settings; @@ -94,76 +97,8 @@ pub fn is_version_compatible( true } -pub trait ExtensionRegistrationHooks: Send + Sync + 'static { - fn remove_user_themes(&self, _themes: Vec) {} - - fn load_user_theme(&self, _theme_path: PathBuf, _fs: Arc) -> Task> { - Task::ready(Ok(())) - } - - fn list_theme_names( - &self, - _theme_path: PathBuf, - _fs: Arc, - ) -> Task>> { - Task::ready(Ok(Vec::new())) - } - - fn reload_current_theme(&self, _cx: &mut AppContext) {} - - fn register_language( - &self, - _language: LanguageName, - _grammar: Option>, - _matcher: language::LanguageMatcher, - _load: Arc Result + 'static + Send + Sync>, - ) { - } - - fn register_lsp_adapter(&self, _language: LanguageName, _adapter: ExtensionLspAdapter) {} - - fn remove_lsp_adapter(&self, _language: &LanguageName, _server_name: &LanguageServerName) {} - - fn register_wasm_grammars(&self, _grammars: Vec<(Arc, PathBuf)>) {} - - fn remove_languages( - &self, - _languages_to_remove: &[LanguageName], - _grammars_to_remove: &[Arc], - ) { - } - - fn register_slash_command( - &self, - _extension: Arc, - _command: extension::SlashCommand, - ) { - } - - fn register_context_server( - &self, - _id: Arc, - _extension: WasmExtension, - _cx: &mut AppContext, - ) { - } - - fn register_docs_provider(&self, _extension: Arc, _provider_id: Arc) {} - - fn register_snippets(&self, _path: &PathBuf, _snippet_contents: &str) -> Result<()> { - Ok(()) - } - - fn update_lsp_status( - &self, - _server_name: lsp::LanguageServerName, - _status: language::LanguageServerBinaryStatus, - ) { - } -} - pub struct ExtensionStore { - pub registration_hooks: Arc, + pub proxy: Arc, pub builder: Arc, pub extension_index: ExtensionIndex, pub fs: Arc, @@ -178,6 +113,8 @@ pub struct ExtensionStore { pub wasm_host: Arc, pub wasm_extensions: Vec<(Arc, WasmExtension)>, pub tasks: Vec>, + pub ssh_clients: HashMap>, + pub ssh_registered_tx: UnboundedSender<()>, } #[derive(Clone, Copy)] @@ -231,7 +168,7 @@ pub struct ExtensionIndexLanguageEntry { actions!(zed, [ReloadExtensions]); pub fn init( - registration_hooks: Arc, + extension_host_proxy: Arc, fs: Arc, client: Arc, node_runtime: NodeRuntime, @@ -243,7 +180,7 @@ pub fn init( ExtensionStore::new( paths::extensions_dir().clone(), None, - registration_hooks, + extension_host_proxy, fs, client.http_client().clone(), client.http_client().clone(), @@ -275,7 +212,7 @@ impl ExtensionStore { pub fn new( extensions_dir: PathBuf, build_dir: Option, - extension_api: Arc, + extension_host_proxy: Arc, fs: Arc, http_client: Arc, builder_client: Arc, @@ -289,8 +226,9 @@ impl ExtensionStore { let index_path = extensions_dir.join("index.json"); let (reload_tx, mut reload_rx) = unbounded(); + let (connection_registered_tx, mut connection_registered_rx) = unbounded(); let mut this = Self { - registration_hooks: extension_api.clone(), + proxy: extension_host_proxy.clone(), extension_index: Default::default(), installed_dir, index_path, @@ -302,7 +240,7 @@ impl ExtensionStore { fs.clone(), http_client.clone(), node_runtime, - extension_api, + extension_host_proxy, work_dir, cx, ), @@ -312,6 +250,9 @@ impl ExtensionStore { telemetry, reload_tx, tasks: Vec::new(), + + ssh_clients: HashMap::default(), + ssh_registered_tx: connection_registered_tx, }; // The extensions store maintains an index file, which contains a complete @@ -337,7 +278,10 @@ impl ExtensionStore { if let (Ok(Some(index_metadata)), Ok(Some(extensions_metadata))) = (index_metadata, extensions_metadata) { - if index_metadata.mtime > extensions_metadata.mtime { + if index_metadata + .mtime + .bad_is_greater_than(extensions_metadata.mtime) + { extension_index_needs_rebuild = false; } } @@ -386,6 +330,14 @@ impl ExtensionStore { .await; index_changed = false; } + + Self::update_ssh_clients(&this, &mut cx).await?; + } + _ = connection_registered_rx.next() => { + debounce_timer = cx + .background_executor() + .timer(RELOAD_DEBOUNCE_DURATION) + .fuse(); } extension_id = reload_rx.next() => { let Some(extension_id) = extension_id else { break; }; @@ -1089,16 +1041,16 @@ impl ExtensionStore { grammars_to_remove.extend(extension.manifest.grammars.keys().cloned()); for (language_server_name, config) in extension.manifest.language_servers.iter() { for language in config.languages() { - self.registration_hooks - .remove_lsp_adapter(&language, language_server_name); + self.proxy + .remove_language_server(&language, language_server_name); } } } self.wasm_extensions .retain(|(extension, _)| !extensions_to_unload.contains(&extension.id)); - self.registration_hooks.remove_user_themes(themes_to_remove); - self.registration_hooks + self.proxy.remove_user_themes(themes_to_remove); + self.proxy .remove_languages(&languages_to_remove, &grammars_to_remove); let languages_to_add = new_index @@ -1133,8 +1085,7 @@ impl ExtensionStore { })); } - self.registration_hooks - .register_wasm_grammars(grammars_to_add); + self.proxy.register_grammars(grammars_to_add); for (language_name, language) in languages_to_add { let mut language_path = self.installed_dir.clone(); @@ -1142,7 +1093,7 @@ impl ExtensionStore { Path::new(language.extension.as_ref()), language.path.as_path(), ]); - self.registration_hooks.register_language( + self.proxy.register_language( language_name.clone(), language.grammar.clone(), language.matcher.clone(), @@ -1172,7 +1123,7 @@ impl ExtensionStore { let fs = self.fs.clone(); let wasm_host = self.wasm_host.clone(); let root_dir = self.installed_dir.clone(); - let api = self.registration_hooks.clone(); + let proxy = self.proxy.clone(); let extension_entries = extensions_to_load .iter() .filter_map(|name| new_index.extensions.get(name).cloned()) @@ -1188,13 +1139,17 @@ impl ExtensionStore { let fs = fs.clone(); async move { for theme_path in themes_to_add.into_iter() { - api.load_user_theme(theme_path, fs.clone()).await.log_err(); + proxy + .load_user_theme(theme_path, fs.clone()) + .await + .log_err(); } for snippets_path in &snippets_to_add { if let Some(snippets_contents) = fs.load(snippets_path).await.log_err() { - api.register_snippets(snippets_path, &snippets_contents) + proxy + .register_snippet(snippets_path, &snippets_contents) .log_err(); } } @@ -1235,19 +1190,16 @@ impl ExtensionStore { for (language_server_id, language_server_config) in &manifest.language_servers { for language in language_server_config.languages() { - this.registration_hooks.register_lsp_adapter( + this.proxy.register_language_server( + extension.clone(), + language_server_id.clone(), language.clone(), - ExtensionLspAdapter { - extension: extension.clone(), - language_server_id: language_server_id.clone(), - language_name: language.clone(), - }, ); } } for (slash_command_name, slash_command) in &manifest.slash_commands { - this.registration_hooks.register_slash_command( + this.proxy.register_slash_command( extension.clone(), extension::SlashCommand { name: slash_command_name.to_string(), @@ -1262,21 +1214,18 @@ impl ExtensionStore { } for (id, _context_server_entry) in &manifest.context_servers { - this.registration_hooks.register_context_server( - id.clone(), - wasm_extension.clone(), - cx, - ); + this.proxy + .register_context_server(extension.clone(), id.clone(), cx); } for (provider_id, _provider) in &manifest.indexed_docs_providers { - this.registration_hooks - .register_docs_provider(extension.clone(), provider_id.clone()); + this.proxy + .register_indexed_docs_provider(extension.clone(), provider_id.clone()); } } this.wasm_extensions.extend(wasm_extensions); - this.registration_hooks.reload_current_theme(cx); + this.proxy.reload_current_theme(cx); }) .ok(); }) @@ -1287,7 +1236,7 @@ impl ExtensionStore { let work_dir = self.wasm_host.work_dir.clone(); let extensions_dir = self.installed_dir.clone(); let index_path = self.index_path.clone(); - let extension_api = self.registration_hooks.clone(); + let proxy = self.proxy.clone(); cx.background_executor().spawn(async move { let start_time = Instant::now(); let mut index = ExtensionIndex::default(); @@ -1313,7 +1262,7 @@ impl ExtensionStore { fs.clone(), extension_dir, &mut index, - extension_api.clone(), + proxy.clone(), ) .await .log_err(); @@ -1336,7 +1285,7 @@ impl ExtensionStore { fs: Arc, extension_dir: PathBuf, index: &mut ExtensionIndex, - extension_api: Arc, + proxy: Arc, ) -> Result<()> { let mut extension_manifest = ExtensionManifest::load(fs.clone(), &extension_dir).await?; let extension_id = extension_manifest.id.clone(); @@ -1388,7 +1337,7 @@ impl ExtensionStore { continue; }; - let Some(theme_families) = extension_api + let Some(theme_families) = proxy .list_theme_names(theme_path.clone(), fs.clone()) .await .log_err() @@ -1431,6 +1380,144 @@ impl ExtensionStore { Ok(()) } + + fn prepare_remote_extension( + &mut self, + extension_id: Arc, + tmp_dir: PathBuf, + cx: &mut ModelContext, + ) -> Task> { + let src_dir = self.extensions_dir().join(extension_id.as_ref()); + let Some(loaded_extension) = self.extension_index.extensions.get(&extension_id).cloned() + else { + return Task::ready(Err(anyhow!("extension no longer installed"))); + }; + let fs = self.fs.clone(); + cx.background_executor().spawn(async move { + for well_known_path in ["extension.toml", "extension.json", "extension.wasm"] { + if fs.is_file(&src_dir.join(well_known_path)).await { + fs.copy_file( + &src_dir.join(well_known_path), + &tmp_dir.join(well_known_path), + fs::CopyOptions::default(), + ) + .await? + } + } + + for language_path in loaded_extension.manifest.languages.iter() { + if fs + .is_file(&src_dir.join(language_path).join("config.toml")) + .await + { + fs.create_dir(&tmp_dir.join(language_path)).await?; + fs.copy_file( + &src_dir.join(language_path).join("config.toml"), + &tmp_dir.join(language_path).join("config.toml"), + fs::CopyOptions::default(), + ) + .await? + } + } + + Ok(()) + }) + } + + async fn sync_extensions_over_ssh( + this: &WeakModel, + client: WeakModel, + cx: &mut AsyncAppContext, + ) -> Result<()> { + let extensions = this.update(cx, |this, _cx| { + this.extension_index + .extensions + .iter() + .filter_map(|(id, entry)| { + if entry.manifest.language_servers.is_empty() { + return None; + } + Some(proto::Extension { + id: id.to_string(), + version: entry.manifest.version.to_string(), + dev: entry.dev, + }) + }) + .collect() + })?; + + let response = client + .update(cx, |client, _cx| { + client + .proto_client() + .request(proto::SyncExtensions { extensions }) + })? + .await?; + + for missing_extension in response.missing_extensions.into_iter() { + let tmp_dir = tempfile::tempdir()?; + this.update(cx, |this, cx| { + this.prepare_remote_extension( + missing_extension.id.clone().into(), + tmp_dir.path().to_owned(), + cx, + ) + })? + .await?; + let dest_dir = PathBuf::from(&response.tmp_dir).join(missing_extension.clone().id); + log::info!("Uploading extension {}", missing_extension.clone().id); + + client + .update(cx, |client, cx| { + client.upload_directory(tmp_dir.path().to_owned(), dest_dir.clone(), cx) + })? + .await?; + + client + .update(cx, |client, _cx| { + client.proto_client().request(proto::InstallExtension { + tmp_dir: dest_dir.to_string_lossy().to_string(), + extension: Some(missing_extension), + }) + })? + .await?; + } + + anyhow::Ok(()) + } + + pub async fn update_ssh_clients( + this: &WeakModel, + cx: &mut AsyncAppContext, + ) -> Result<()> { + let clients = this.update(cx, |this, _cx| { + this.ssh_clients.retain(|_k, v| v.upgrade().is_some()); + this.ssh_clients.values().cloned().collect::>() + })?; + + for client in clients { + Self::sync_extensions_over_ssh(&this, client, cx) + .await + .log_err(); + } + + anyhow::Ok(()) + } + + pub fn register_ssh_client( + &mut self, + client: Model, + cx: &mut ModelContext, + ) { + let connection_options = client.read(cx).connection_options(); + if self.ssh_clients.contains_key(&connection_options.ssh_url()) { + return; + } + + self.ssh_clients + .insert(connection_options.ssh_url(), client.downgrade()); + self.ssh_registered_tx.unbounded_send(()).ok(); + } } fn load_plugin_queries(root_path: &Path) -> LanguageQueries { diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs index 23004e9d7f..1359b5b202 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -1,17 +1,16 @@ -use crate::extension_lsp_adapter::ExtensionLspAdapter; use crate::{ Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry, ExtensionIndexThemeEntry, ExtensionManifest, ExtensionSettings, ExtensionStore, GrammarManifestEntry, SchemaVersion, RELOAD_DEBOUNCE_DURATION, }; -use anyhow::Result; use async_compression::futures::bufread::GzipEncoder; use collections::BTreeMap; +use extension::ExtensionHostProxy; use fs::{FakeFs, Fs, RealFs}; use futures::{io::BufReader, AsyncReadExt, StreamExt}; -use gpui::{BackgroundExecutor, Context, SemanticVersion, SharedString, Task, TestAppContext}; +use gpui::{Context, SemanticVersion, TestAppContext}; use http_client::{FakeHttpClient, Response}; -use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage}; +use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus}; use lsp::LanguageServerName; use node_runtime::NodeRuntime; use parking_lot::Mutex; @@ -28,84 +27,6 @@ use std::{ use theme::ThemeRegistry; use util::test::temp_tree; -use crate::ExtensionRegistrationHooks; - -struct TestExtensionRegistrationHooks { - executor: BackgroundExecutor, - language_registry: Arc, - theme_registry: Arc, -} - -impl ExtensionRegistrationHooks for TestExtensionRegistrationHooks { - fn list_theme_names(&self, path: PathBuf, fs: Arc) -> Task>> { - self.executor.spawn(async move { - let themes = theme::read_user_theme(&path, fs).await?; - Ok(themes.themes.into_iter().map(|theme| theme.name).collect()) - }) - } - - fn load_user_theme(&self, theme_path: PathBuf, fs: Arc) -> Task> { - let theme_registry = self.theme_registry.clone(); - self.executor - .spawn(async move { theme_registry.load_user_theme(&theme_path, fs).await }) - } - - fn remove_user_themes(&self, themes: Vec) { - self.theme_registry.remove_user_themes(&themes); - } - - fn register_language( - &self, - language: language::LanguageName, - grammar: Option>, - matcher: language::LanguageMatcher, - load: Arc Result + 'static + Send + Sync>, - ) { - self.language_registry - .register_language(language, grammar, matcher, load) - } - - fn remove_languages( - &self, - languages_to_remove: &[language::LanguageName], - grammars_to_remove: &[Arc], - ) { - self.language_registry - .remove_languages(&languages_to_remove, &grammars_to_remove); - } - - fn register_wasm_grammars(&self, grammars: Vec<(Arc, PathBuf)>) { - self.language_registry.register_wasm_grammars(grammars) - } - - fn register_lsp_adapter( - &self, - language_name: language::LanguageName, - adapter: ExtensionLspAdapter, - ) { - self.language_registry - .register_lsp_adapter(language_name, Arc::new(adapter)); - } - - fn update_lsp_status( - &self, - server_name: lsp::LanguageServerName, - status: LanguageServerBinaryStatus, - ) { - self.language_registry - .update_lsp_status(server_name, status); - } - - fn remove_lsp_adapter( - &self, - language_name: &language::LanguageName, - server_name: &lsp::LanguageServerName, - ) { - self.language_registry - .remove_lsp_adapter(language_name, server_name); - } -} - #[cfg(test)] #[ctor::ctor] fn init_logger() { @@ -337,20 +258,18 @@ async fn test_extension_store(cx: &mut TestAppContext) { .collect(), }; - let language_registry = Arc::new(LanguageRegistry::test(cx.executor())); + let proxy = Arc::new(ExtensionHostProxy::new()); let theme_registry = Arc::new(ThemeRegistry::new(Box::new(()))); - let registration_hooks = Arc::new(TestExtensionRegistrationHooks { - executor: cx.executor(), - language_registry: language_registry.clone(), - theme_registry: theme_registry.clone(), - }); + theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor()); + let language_registry = Arc::new(LanguageRegistry::test(cx.executor())); + language_extension::init(proxy.clone(), language_registry.clone()); let node_runtime = NodeRuntime::unavailable(); let store = cx.new_model(|cx| { ExtensionStore::new( PathBuf::from("/the-extension-dir"), None, - registration_hooks.clone(), + proxy.clone(), fs.clone(), http_client.clone(), http_client.clone(), @@ -475,7 +394,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { ExtensionStore::new( PathBuf::from("/the-extension-dir"), None, - registration_hooks, + proxy, fs.clone(), http_client.clone(), http_client.clone(), @@ -558,13 +477,11 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { let project = Project::test(fs.clone(), [project_dir.as_path()], cx).await; - let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); + let proxy = Arc::new(ExtensionHostProxy::new()); let theme_registry = Arc::new(ThemeRegistry::new(Box::new(()))); - let registration_hooks = Arc::new(TestExtensionRegistrationHooks { - executor: cx.executor(), - language_registry: language_registry.clone(), - theme_registry: theme_registry.clone(), - }); + theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor()); + let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); + language_extension::init(proxy.clone(), language_registry.clone()); let node_runtime = NodeRuntime::unavailable(); let mut status_updates = language_registry.language_server_binary_statuses(); @@ -658,7 +575,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { ExtensionStore::new( extensions_dir.clone(), Some(cache_dir), - registration_hooks, + proxy, fs.clone(), extension_client.clone(), builder_client, diff --git a/crates/extension_host/src/headless_host.rs b/crates/extension_host/src/headless_host.rs new file mode 100644 index 0000000000..19a574b9d4 --- /dev/null +++ b/crates/extension_host/src/headless_host.rs @@ -0,0 +1,319 @@ +use std::{path::PathBuf, sync::Arc}; + +use anyhow::{anyhow, Context as _, Result}; +use client::{proto, TypedEnvelope}; +use collections::{HashMap, HashSet}; +use extension::{ + Extension, ExtensionHostProxy, ExtensionLanguageProxy, ExtensionLanguageServerProxy, + ExtensionManifest, +}; +use fs::{Fs, RemoveOptions, RenameOptions}; +use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext, Task, WeakModel}; +use http_client::HttpClient; +use language::{LanguageConfig, LanguageName, LanguageQueries, LoadedLanguage}; +use lsp::LanguageServerName; +use node_runtime::NodeRuntime; + +use crate::wasm_host::{WasmExtension, WasmHost}; + +#[derive(Clone, Debug)] +pub struct ExtensionVersion { + pub id: String, + pub version: String, + pub dev: bool, +} + +pub struct HeadlessExtensionStore { + pub fs: Arc, + pub extension_dir: PathBuf, + pub proxy: Arc, + pub wasm_host: Arc, + pub loaded_extensions: HashMap, Arc>, + pub loaded_languages: HashMap, Vec>, + pub loaded_language_servers: HashMap, Vec<(LanguageServerName, LanguageName)>>, +} + +impl HeadlessExtensionStore { + pub fn new( + fs: Arc, + http_client: Arc, + extension_dir: PathBuf, + extension_host_proxy: Arc, + node_runtime: NodeRuntime, + cx: &mut AppContext, + ) -> Model { + cx.new_model(|cx| Self { + fs: fs.clone(), + wasm_host: WasmHost::new( + fs.clone(), + http_client.clone(), + node_runtime, + extension_host_proxy.clone(), + extension_dir.join("work"), + cx, + ), + extension_dir, + proxy: extension_host_proxy, + loaded_extensions: Default::default(), + loaded_languages: Default::default(), + loaded_language_servers: Default::default(), + }) + } + + pub fn sync_extensions( + &mut self, + extensions: Vec, + cx: &ModelContext, + ) -> Task>> { + let on_client = HashSet::from_iter(extensions.iter().map(|e| e.id.as_str())); + let to_remove: Vec> = self + .loaded_extensions + .keys() + .filter(|id| !on_client.contains(id.as_ref())) + .cloned() + .collect(); + let to_load: Vec = extensions + .into_iter() + .filter(|e| { + if e.dev { + return true; + } + !self + .loaded_extensions + .get(e.id.as_str()) + .is_some_and(|loaded| loaded.as_ref() == e.version.as_str()) + }) + .collect(); + + cx.spawn(|this, mut cx| async move { + let mut missing = Vec::new(); + + for extension_id in to_remove { + log::info!("removing extension: {}", extension_id); + this.update(&mut cx, |this, cx| { + this.uninstall_extension(&extension_id, cx) + })? + .await?; + } + + for extension in to_load { + if let Err(e) = Self::load_extension(this.clone(), extension.clone(), &mut cx).await + { + log::info!("failed to load extension: {}, {:?}", extension.id, e); + missing.push(extension) + } else if extension.dev { + missing.push(extension) + } + } + + Ok(missing) + }) + } + + pub async fn load_extension( + this: WeakModel, + extension: ExtensionVersion, + cx: &mut AsyncAppContext, + ) -> Result<()> { + let (fs, wasm_host, extension_dir) = this.update(cx, |this, _cx| { + this.loaded_extensions.insert( + extension.id.clone().into(), + extension.version.clone().into(), + ); + ( + this.fs.clone(), + this.wasm_host.clone(), + this.extension_dir.join(&extension.id), + ) + })?; + + let manifest = Arc::new(ExtensionManifest::load(fs.clone(), &extension_dir).await?); + + debug_assert!(!manifest.languages.is_empty() || !manifest.language_servers.is_empty()); + + if manifest.version.as_ref() != extension.version.as_str() { + anyhow::bail!( + "mismatched versions: ({}) != ({})", + manifest.version, + extension.version + ) + } + + for language_path in &manifest.languages { + let language_path = extension_dir.join(language_path); + let config = fs.load(&language_path.join("config.toml")).await?; + let mut config = ::toml::from_str::(&config)?; + + this.update(cx, |this, _cx| { + this.loaded_languages + .entry(manifest.id.clone()) + .or_default() + .push(config.name.clone()); + + config.grammar = None; + + this.proxy.register_language( + config.name.clone(), + None, + config.matcher.clone(), + Arc::new(move || { + Ok(LoadedLanguage { + config: config.clone(), + queries: LanguageQueries::default(), + context_provider: None, + toolchain_provider: None, + }) + }), + ); + })?; + } + + if manifest.language_servers.is_empty() { + return Ok(()); + } + + let wasm_extension: Arc = + Arc::new(WasmExtension::load(extension_dir, &manifest, wasm_host.clone(), &cx).await?); + + for (language_server_id, language_server_config) in &manifest.language_servers { + for language in language_server_config.languages() { + this.update(cx, |this, _cx| { + this.loaded_language_servers + .entry(manifest.id.clone()) + .or_default() + .push((language_server_id.clone(), language.clone())); + this.proxy.register_language_server( + wasm_extension.clone(), + language_server_id.clone(), + language.clone(), + ); + })?; + } + } + + Ok(()) + } + + fn uninstall_extension( + &mut self, + extension_id: &Arc, + cx: &mut ModelContext, + ) -> Task> { + self.loaded_extensions.remove(extension_id); + + let languages_to_remove = self + .loaded_languages + .remove(extension_id) + .unwrap_or_default(); + self.proxy.remove_languages(&languages_to_remove, &[]); + + for (language_server_name, language) in self + .loaded_language_servers + .remove(extension_id) + .unwrap_or_default() + { + self.proxy + .remove_language_server(&language, &language_server_name); + } + + let path = self.extension_dir.join(&extension_id.to_string()); + let fs = self.fs.clone(); + cx.spawn(|_, _| async move { + fs.remove_dir( + &path, + RemoveOptions { + recursive: true, + ignore_if_not_exists: true, + }, + ) + .await + }) + } + + pub fn install_extension( + &mut self, + extension: ExtensionVersion, + tmp_path: PathBuf, + cx: &mut ModelContext, + ) -> Task> { + let path = self.extension_dir.join(&extension.id); + let fs = self.fs.clone(); + + cx.spawn(|this, mut cx| async move { + if fs.is_dir(&path).await { + this.update(&mut cx, |this, cx| { + this.uninstall_extension(&extension.id.clone().into(), cx) + })? + .await?; + } + + fs.rename(&tmp_path, &path, RenameOptions::default()) + .await?; + + Self::load_extension(this, extension, &mut cx).await + }) + } + + pub async fn handle_sync_extensions( + extension_store: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result { + let requested_extensions = + envelope + .payload + .extensions + .into_iter() + .map(|p| ExtensionVersion { + id: p.id, + version: p.version, + dev: p.dev, + }); + let missing_extensions = extension_store + .update(&mut cx, |extension_store, cx| { + extension_store.sync_extensions(requested_extensions.collect(), cx) + })? + .await?; + + Ok(proto::SyncExtensionsResponse { + missing_extensions: missing_extensions + .into_iter() + .map(|e| proto::Extension { + id: e.id, + version: e.version, + dev: e.dev, + }) + .collect(), + tmp_dir: paths::remote_extensions_uploads_dir() + .to_string_lossy() + .to_string(), + }) + } + + pub async fn handle_install_extension( + extensions: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result { + let extension = envelope + .payload + .extension + .with_context(|| anyhow!("Invalid InstallExtension request"))?; + + extensions + .update(&mut cx, |extensions, cx| { + extensions.install_extension( + ExtensionVersion { + id: extension.id, + version: extension.version, + dev: extension.dev, + }, + PathBuf::from(envelope.payload.tmp_dir), + cx, + ) + })? + .await?; + + Ok(proto::Ack {}) + } +} diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index 54699ac0a1..766ca8c0bb 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -1,11 +1,11 @@ pub mod wit; -use crate::{ExtensionManifest, ExtensionRegistrationHooks}; +use crate::ExtensionManifest; use anyhow::{anyhow, bail, Context as _, Result}; use async_trait::async_trait; use extension::{ - CodeLabel, Command, Completion, KeyValueStoreDelegate, SlashCommand, - SlashCommandArgumentCompletion, SlashCommandOutput, Symbol, WorktreeDelegate, + CodeLabel, Command, Completion, ExtensionHostProxy, KeyValueStoreDelegate, ProjectDelegate, + SlashCommand, SlashCommandArgumentCompletion, SlashCommandOutput, Symbol, WorktreeDelegate, }; use fs::{normalize_path, Fs}; use futures::future::LocalBoxFuture; @@ -34,14 +34,13 @@ use wasmtime::{ }; use wasmtime_wasi::{self as wasi, WasiView}; use wit::Extension; -pub use wit::ExtensionProject; pub struct WasmHost { engine: Engine, release_channel: ReleaseChannel, http_client: Arc, node_runtime: NodeRuntime, - pub registration_hooks: Arc, + pub(crate) proxy: Arc, fs: Arc, pub work_dir: PathBuf, _main_thread_message_task: Task<()>, @@ -238,6 +237,25 @@ impl extension::Extension for WasmExtension { .await } + async fn context_server_command( + &self, + context_server_id: Arc, + project: Arc, + ) -> Result { + self.call(|extension, store| { + async move { + let project_resource = store.data_mut().table().push(project)?; + let command = extension + .call_context_server_command(store, context_server_id.clone(), project_resource) + .await? + .map_err(|err| anyhow!("{err}"))?; + anyhow::Ok(command.into()) + } + .boxed() + }) + .await + } + async fn suggest_docs_packages(&self, provider: Arc) -> Result> { self.call(|extension, store| { async move { @@ -312,7 +330,7 @@ impl WasmHost { fs: Arc, http_client: Arc, node_runtime: NodeRuntime, - registration_hooks: Arc, + proxy: Arc, work_dir: PathBuf, cx: &mut AppContext, ) -> Arc { @@ -328,7 +346,7 @@ impl WasmHost { work_dir, http_client, node_runtime, - registration_hooks, + proxy, release_channel: ReleaseChannel::global(cx), _main_thread_message_task: task, main_thread_message_tx: tx, diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs index bd1770de38..1f0891b410 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs @@ -3,7 +3,7 @@ use crate::wasm_host::wit::since_v0_0_4; use crate::wasm_host::WasmState; use anyhow::Result; use async_trait::async_trait; -use extension::WorktreeDelegate; +use extension::{ExtensionLanguageServerProxy, WorktreeDelegate}; use language::LanguageServerBinaryStatus; use semantic_version::SemanticVersion; use std::sync::{Arc, OnceLock}; @@ -149,8 +149,9 @@ impl ExtensionImports for WasmState { }; self.host - .registration_hooks - .update_lsp_status(lsp::LanguageServerName(server_name.into()), status); + .proxy + .update_language_server_status(lsp::LanguageServerName(server_name.into()), status); + Ok(()) } diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs index 18f4bc0234..c1c07a2b09 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs @@ -5,7 +5,7 @@ use anyhow::{anyhow, bail, Context, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use async_trait::async_trait; -use extension::{KeyValueStoreDelegate, WorktreeDelegate}; +use extension::{ExtensionLanguageServerProxy, KeyValueStoreDelegate, WorktreeDelegate}; use futures::{io::BufReader, FutureExt as _}; use futures::{lock::Mutex, AsyncReadExt}; use language::LanguageName; @@ -495,8 +495,9 @@ impl ExtensionImports for WasmState { }; self.host - .registration_hooks - .update_lsp_status(::lsp::LanguageServerName(server_name.into()), status); + .proxy + .update_language_server_status(::lsp::LanguageServerName(server_name.into()), status); + Ok(()) } diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs index 02577abd0e..f7e11e1032 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs @@ -8,7 +8,9 @@ use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use async_trait::async_trait; use context_servers::manager::ContextServerSettings; -use extension::{KeyValueStoreDelegate, WorktreeDelegate}; +use extension::{ + ExtensionLanguageServerProxy, KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate, +}; use futures::{io::BufReader, FutureExt as _}; use futures::{lock::Mutex, AsyncReadExt}; use language::{language_settings::AllLanguageSettings, LanguageName, LanguageServerBinaryStatus}; @@ -44,13 +46,10 @@ mod settings { } pub type ExtensionWorktree = Arc; +pub type ExtensionProject = Arc; pub type ExtensionKeyValueStore = Arc; pub type ExtensionHttpResponseStream = Arc>>; -pub struct ExtensionProject { - pub worktree_ids: Vec, -} - pub fn linker() -> &'static Linker { static LINKER: OnceLock> = OnceLock::new(); LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker)) @@ -273,7 +272,7 @@ impl HostProject for WasmState { project: Resource, ) -> wasmtime::Result> { let project = self.table.get(&project)?; - Ok(project.worktree_ids.clone()) + Ok(project.worktree_ids()) } fn drop(&mut self, _project: Resource) -> Result<()> { @@ -685,8 +684,9 @@ impl ExtensionImports for WasmState { }; self.host - .registration_hooks - .update_lsp_status(::lsp::LanguageServerName(server_name.into()), status); + .proxy + .update_language_server_status(::lsp::LanguageServerName(server_name.into()), status); + Ok(()) } diff --git a/crates/extensions_ui/Cargo.toml b/crates/extensions_ui/Cargo.toml index 9709aa7a2b..cc6e78d6f3 100644 --- a/crates/extensions_ui/Cargo.toml +++ b/crates/extensions_ui/Cargo.toml @@ -13,21 +13,15 @@ path = "src/extensions_ui.rs" [dependencies] anyhow.workspace = true -assistant_slash_command.workspace = true client.workspace = true collections.workspace = true -context_servers.workspace = true db.workspace = true editor.workspace = true -extension.workspace = true extension_host.workspace = true fs.workspace = true fuzzy.workspace = true gpui.workspace = true -indexed_docs.workspace = true language.workspace = true -log.workspace = true -lsp.workspace = true num-format.workspace = true picker.workspace = true project.workspace = true @@ -36,14 +30,12 @@ semantic_version.workspace = true serde.workspace = true settings.workspace = true smallvec.workspace = true -snippet_provider.workspace = true theme.workspace = true -theme_selector.workspace = true ui.workspace = true util.workspace = true -vim.workspace = true -wasmtime-wasi.workspace = true +vim_mode_setting.workspace = true workspace.workspace = true +zed_actions.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } diff --git a/crates/extensions_ui/src/extension_registration_hooks.rs b/crates/extensions_ui/src/extension_registration_hooks.rs deleted file mode 100644 index f8cd9a3429..0000000000 --- a/crates/extensions_ui/src/extension_registration_hooks.rs +++ /dev/null @@ -1,212 +0,0 @@ -use std::{path::PathBuf, sync::Arc}; - -use anyhow::{anyhow, Result}; -use assistant_slash_command::{ExtensionSlashCommand, SlashCommandRegistry}; -use context_servers::manager::ServerCommand; -use context_servers::ContextServerFactoryRegistry; -use db::smol::future::FutureExt as _; -use extension::Extension; -use extension_host::wasm_host::ExtensionProject; -use extension_host::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host}; -use fs::Fs; -use gpui::{AppContext, BackgroundExecutor, Model, Task}; -use indexed_docs::{ExtensionIndexedDocsProvider, IndexedDocsRegistry, ProviderId}; -use language::{LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage}; -use snippet_provider::SnippetRegistry; -use theme::{ThemeRegistry, ThemeSettings}; -use ui::SharedString; -use wasmtime_wasi::WasiView as _; - -pub struct ConcreteExtensionRegistrationHooks { - slash_command_registry: Arc, - theme_registry: Arc, - indexed_docs_registry: Arc, - snippet_registry: Arc, - language_registry: Arc, - context_server_factory_registry: Model, - executor: BackgroundExecutor, -} - -impl ConcreteExtensionRegistrationHooks { - pub fn new( - theme_registry: Arc, - slash_command_registry: Arc, - indexed_docs_registry: Arc, - snippet_registry: Arc, - language_registry: Arc, - context_server_factory_registry: Model, - cx: &AppContext, - ) -> Arc { - Arc::new(Self { - theme_registry, - slash_command_registry, - indexed_docs_registry, - snippet_registry, - language_registry, - context_server_factory_registry, - executor: cx.background_executor().clone(), - }) - } -} - -impl extension_host::ExtensionRegistrationHooks for ConcreteExtensionRegistrationHooks { - fn remove_user_themes(&self, themes: Vec) { - self.theme_registry.remove_user_themes(&themes); - } - - fn load_user_theme(&self, theme_path: PathBuf, fs: Arc) -> Task> { - let theme_registry = self.theme_registry.clone(); - self.executor - .spawn(async move { theme_registry.load_user_theme(&theme_path, fs).await }) - } - - fn register_slash_command( - &self, - extension: Arc, - command: extension::SlashCommand, - ) { - self.slash_command_registry - .register_command(ExtensionSlashCommand::new(extension, command), false) - } - - fn register_context_server( - &self, - id: Arc, - extension: wasm_host::WasmExtension, - cx: &mut AppContext, - ) { - self.context_server_factory_registry - .update(cx, |registry, _| { - registry.register_server_factory( - id.clone(), - Arc::new({ - move |project, cx| { - log::info!( - "loading command for context server {id} from extension {}", - extension.manifest.id - ); - - let id = id.clone(); - let extension = extension.clone(); - cx.spawn(|mut cx| async move { - let extension_project = - project.update(&mut cx, |project, cx| ExtensionProject { - worktree_ids: project - .visible_worktrees(cx) - .map(|worktree| worktree.read(cx).id().to_proto()) - .collect(), - })?; - - let command = extension - .call({ - let id = id.clone(); - |extension, store| { - async move { - let project = store - .data_mut() - .table() - .push(extension_project)?; - let command = extension - .call_context_server_command( - store, - id.clone(), - project, - ) - .await? - .map_err(|e| anyhow!("{}", e))?; - anyhow::Ok(command) - } - .boxed() - } - }) - .await?; - - log::info!("loaded command for context server {id}: {command:?}"); - - Ok(ServerCommand { - path: command.command, - args: command.args, - env: Some(command.env.into_iter().collect()), - }) - }) - } - }), - ) - }); - } - - fn register_docs_provider(&self, extension: Arc, provider_id: Arc) { - self.indexed_docs_registry - .register_provider(Box::new(ExtensionIndexedDocsProvider::new( - extension, - ProviderId(provider_id), - ))); - } - - fn register_snippets(&self, path: &PathBuf, snippet_contents: &str) -> Result<()> { - self.snippet_registry - .register_snippets(path, snippet_contents) - } - - fn update_lsp_status( - &self, - server_name: lsp::LanguageServerName, - status: LanguageServerBinaryStatus, - ) { - self.language_registry - .update_lsp_status(server_name, status); - } - - fn register_lsp_adapter( - &self, - language_name: language::LanguageName, - adapter: ExtensionLspAdapter, - ) { - self.language_registry - .register_lsp_adapter(language_name, Arc::new(adapter)); - } - - fn remove_lsp_adapter( - &self, - language_name: &language::LanguageName, - server_name: &lsp::LanguageServerName, - ) { - self.language_registry - .remove_lsp_adapter(language_name, server_name); - } - - fn remove_languages( - &self, - languages_to_remove: &[language::LanguageName], - grammars_to_remove: &[Arc], - ) { - self.language_registry - .remove_languages(&languages_to_remove, &grammars_to_remove); - } - - fn register_wasm_grammars(&self, grammars: Vec<(Arc, PathBuf)>) { - self.language_registry.register_wasm_grammars(grammars) - } - - fn register_language( - &self, - language: language::LanguageName, - grammar: Option>, - matcher: language::LanguageMatcher, - load: Arc Result + 'static + Send + Sync>, - ) { - self.language_registry - .register_language(language, grammar, matcher, load) - } - - fn reload_current_theme(&self, cx: &mut AppContext) { - ThemeSettings::reload_current_theme(cx) - } - - fn list_theme_names(&self, path: PathBuf, fs: Arc) -> Task>> { - self.executor.spawn(async move { - let themes = theme::read_user_theme(&path, fs).await?; - Ok(themes.themes.into_iter().map(|theme| theme.name).collect()) - }) - } -} diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index c2ef9cf9e6..eaffdafa41 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -1,10 +1,7 @@ mod components; -mod extension_registration_hooks; mod extension_suggest; mod extension_version_selector; -pub use extension_registration_hooks::ConcreteExtensionRegistrationHooks; - use std::ops::DerefMut; use std::sync::OnceLock; use std::time::Duration; @@ -17,9 +14,9 @@ use editor::{Editor, EditorElement, EditorStyle}; use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - actions, uniform_list, AppContext, EventEmitter, Flatten, FocusableView, InteractiveElement, - KeyContext, ParentElement, Render, Styled, Task, TextStyle, UniformListScrollHandle, View, - ViewContext, VisualContext, WeakView, WindowContext, + actions, uniform_list, Action, AppContext, EventEmitter, Flatten, FocusableView, + InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle, + UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext, }; use num_format::{Locale, ToFormattedString}; use project::DirectoryLister; @@ -27,7 +24,7 @@ use release_channel::ReleaseChannel; use settings::Settings; use theme::ThemeSettings; use ui::{prelude::*, CheckboxWithLabel, ContextMenu, PopoverMenu, ToggleButton, Tooltip}; -use vim::VimModeSetting; +use vim_mode_setting::VimModeSetting; use workspace::{ item::{Item, ItemEvent}, Workspace, WorkspaceId, @@ -38,12 +35,12 @@ use crate::extension_version_selector::{ ExtensionVersionSelector, ExtensionVersionSelectorDelegate, }; -actions!(zed, [Extensions, InstallDevExtension]); +actions!(zed, [InstallDevExtension]); pub fn init(cx: &mut AppContext) { cx.observe_new_views(move |workspace: &mut Workspace, cx| { workspace - .register_action(move |workspace, _: &Extensions, cx| { + .register_action(move |workspace, _: &zed_actions::Extensions, cx| { let existing = workspace .active_pane() .read(cx) @@ -254,14 +251,13 @@ impl ExtensionsPage { .collect::>(); if !themes.is_empty() { workspace - .update(cx, |workspace, cx| { - theme_selector::toggle( - workspace, - &theme_selector::Toggle { + .update(cx, |_workspace, cx| { + cx.dispatch_action( + zed_actions::theme_selector::Toggle { themes_filter: Some(themes), - }, - cx, - ) + } + .boxed_clone(), + ); }) .ok(); } diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index 286acdfc98..416971b36e 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -39,6 +39,16 @@ pub trait FeatureFlag { } } +pub struct Assistant2FeatureFlag; + +impl FeatureFlag for Assistant2FeatureFlag { + const NAME: &'static str = "assistant2"; + + fn enabled_for_staff() -> bool { + false + } +} + pub struct Remoting {} impl FeatureFlag for Remoting { const NAME: &'static str = "remoting"; diff --git a/crates/feedback/Cargo.toml b/crates/feedback/Cargo.toml index 0447858ca5..605b572c6c 100644 --- a/crates/feedback/Cargo.toml +++ b/crates/feedback/Cargo.toml @@ -22,8 +22,8 @@ db.workspace = true editor.workspace = true futures.workspace = true gpui.workspace = true -human_bytes = "0.4.1" http_client.workspace = true +human_bytes = "0.4.1" language.workspace = true log.workspace = true menu.workspace = true @@ -39,6 +39,7 @@ ui.workspace = true urlencoding = "2.1.2" util.workspace = true workspace.workspace = true +zed_actions.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } diff --git a/crates/feedback/src/feedback.rs b/crates/feedback/src/feedback.rs index 671dea8689..f802a0950d 100644 --- a/crates/feedback/src/feedback.rs +++ b/crates/feedback/src/feedback.rs @@ -5,8 +5,6 @@ use workspace::Workspace; pub mod feedback_modal; -actions!(feedback, [GiveFeedback, SubmitFeedback]); - mod system_specs; actions!( diff --git a/crates/feedback/src/feedback_modal.rs b/crates/feedback/src/feedback_modal.rs index 5270492aee..2c98267ccf 100644 --- a/crates/feedback/src/feedback_modal.rs +++ b/crates/feedback/src/feedback_modal.rs @@ -18,8 +18,9 @@ use serde_derive::Serialize; use ui::{prelude::*, Button, ButtonStyle, IconPosition, Tooltip}; use util::ResultExt; use workspace::{DismissDecision, ModalView, Workspace}; +use zed_actions::feedback::GiveFeedback; -use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedRepo}; +use crate::{system_specs::SystemSpecs, OpenZedRepo}; // For UI testing purposes const SEND_SUCCESS_IN_DEV_MODE: bool = true; diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 286b154f38..6a758211f8 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -10,11 +10,11 @@ pub use open_path_prompt::OpenPathDelegate; use collections::HashMap; use editor::{scroll::Autoscroll, Bias, Editor}; -use file_finder_settings::FileFinderSettings; +use file_finder_settings::{FileFinderSettings, FileFinderWidth}; use file_icons::FileIcons; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ - actions, rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, + actions, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, KeyContext, Model, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, }; @@ -42,7 +42,7 @@ use workspace::{ Workspace, }; -actions!(file_finder, [SelectPrev, OpenMenu]); +actions!(file_finder, [SelectPrev, ToggleMenu]); impl ModalView for FileFinder { fn on_before_dismiss(&mut self, cx: &mut ViewContext) -> workspace::DismissDecision { @@ -189,10 +189,12 @@ impl FileFinder { cx.dispatch_action(Box::new(menu::SelectPrev)); } - fn handle_open_menu(&mut self, _: &OpenMenu, cx: &mut ViewContext) { + fn handle_toggle_menu(&mut self, _: &ToggleMenu, cx: &mut ViewContext) { self.picker.update(cx, |picker, cx| { let menu_handle = &picker.delegate.popover_menu_handle; - if !menu_handle.is_deployed() { + if menu_handle.is_deployed() { + menu_handle.hide(cx); + } else { menu_handle.show(cx); } }); @@ -244,6 +246,22 @@ impl FileFinder { } }) } + + pub fn modal_max_width( + width_setting: Option, + cx: &mut ViewContext, + ) -> Pixels { + let window_width = cx.viewport_size().width; + let small_width = Pixels(545.); + + match width_setting { + None | Some(FileFinderWidth::Small) => small_width, + Some(FileFinderWidth::Full) => window_width, + Some(FileFinderWidth::XLarge) => (window_width - Pixels(512.)).max(small_width), + Some(FileFinderWidth::Large) => (window_width - Pixels(768.)).max(small_width), + Some(FileFinderWidth::Medium) => (window_width - Pixels(1024.)).max(small_width), + } + } } impl EventEmitter for FileFinder {} @@ -257,12 +275,16 @@ impl FocusableView for FileFinder { impl Render for FileFinder { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let key_context = self.picker.read(cx).delegate.key_context(cx); + + let file_finder_settings = FileFinderSettings::get_global(cx); + let modal_max_width = Self::modal_max_width(file_finder_settings.modal_max_width, cx); + v_flex() .key_context(key_context) - .w(rems(34.)) + .w(modal_max_width) .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed)) .on_action(cx.listener(Self::handle_select_prev)) - .on_action(cx.listener(Self::handle_open_menu)) + .on_action(cx.listener(Self::handle_toggle_menu)) .on_action(cx.listener(Self::go_to_file_split_left)) .on_action(cx.listener(Self::go_to_file_split_right)) .on_action(cx.listener(Self::go_to_file_split_up)) @@ -1222,6 +1244,7 @@ impl PickerDelegate for FileFinderDelegate { } fn render_footer(&self, cx: &mut ViewContext>) -> Option { + let context = self.focus_handle.clone(); Some( h_flex() .w_full() @@ -1243,19 +1266,19 @@ impl PickerDelegate for FileFinderDelegate { .trigger( Button::new("actions-trigger", "Split Options") .selected_label_color(Color::Accent) - .key_binding(KeyBinding::for_action_in( - &OpenMenu, - &self.focus_handle, - cx, - )), + .key_binding(KeyBinding::for_action_in(&ToggleMenu, &context, cx)), ) .menu({ move |cx| { - Some(ContextMenu::build(cx, move |menu, _| { - menu.action("Split Left", pane::SplitLeft.boxed_clone()) - .action("Split Right", pane::SplitRight.boxed_clone()) - .action("Split Up", pane::SplitUp.boxed_clone()) - .action("Split Down", pane::SplitDown.boxed_clone()) + Some(ContextMenu::build(cx, { + let context = context.clone(); + move |menu, _| { + menu.context(context) + .action("Split Left", pane::SplitLeft.boxed_clone()) + .action("Split Right", pane::SplitRight.boxed_clone()) + .action("Split Up", pane::SplitUp.boxed_clone()) + .action("Split Down", pane::SplitDown.boxed_clone()) + } })) } }), diff --git a/crates/file_finder/src/file_finder_settings.rs b/crates/file_finder/src/file_finder_settings.rs index c02008c917..0512021d87 100644 --- a/crates/file_finder/src/file_finder_settings.rs +++ b/crates/file_finder/src/file_finder_settings.rs @@ -6,6 +6,7 @@ use settings::{Settings, SettingsSources}; #[derive(Deserialize, Debug, Clone, Copy, PartialEq)] pub struct FileFinderSettings { pub file_icons: bool, + pub modal_max_width: Option, } #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] @@ -14,6 +15,10 @@ pub struct FileFinderSettingsContent { /// /// Default: true pub file_icons: Option, + /// Determines how much space the file finder can take up in relation to the available window width. + /// + /// Default: small + pub modal_max_width: Option, } impl Settings for FileFinderSettings { @@ -25,3 +30,14 @@ impl Settings for FileFinderSettings { sources.json_merge() } } + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum FileFinderWidth { + #[default] + Small, + Medium, + Large, + XLarge, + Full, +} diff --git a/crates/file_finder/src/new_path_prompt.rs b/crates/file_finder/src/new_path_prompt.rs index d4492857b4..6a1b08e205 100644 --- a/crates/file_finder/src/new_path_prompt.rs +++ b/crates/file_finder/src/new_path_prompt.rs @@ -71,8 +71,16 @@ impl Match { fn project_path(&self, project: &Project, cx: &WindowContext) -> Option { let worktree_id = if let Some(path_match) = &self.path_match { WorktreeId::from_usize(path_match.worktree_id) + } else if let Some(worktree) = project.visible_worktrees(cx).find(|worktree| { + worktree + .read(cx) + .root_entry() + .is_some_and(|entry| entry.is_dir()) + }) { + worktree.read(cx).id() } else { - project.worktrees(cx).next()?.read(cx).id() + // todo(): we should find_or_create a workspace. + return None; }; let path = PathBuf::from(self.relative_path()); diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index a9dbb751b6..7a1cfaeaa5 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -24,6 +24,7 @@ libc.workspace = true parking_lot.workspace = true paths.workspace = true rope.workspace = true +proto.workspace = true serde.workspace = true serde_json.workspace = true smol.workspace = true diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 268a9d3f32..fc0fae3fe8 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -27,13 +27,14 @@ use futures::{future::BoxFuture, AsyncRead, Stream, StreamExt}; use git::repository::{GitRepository, RealGitRepository}; use gpui::{AppContext, Global, ReadGlobal}; use rope::Rope; +use serde::{Deserialize, Serialize}; use smol::io::AsyncWriteExt; use std::{ io::{self, Write}, path::{Component, Path, PathBuf}, pin::Pin, sync::Arc, - time::{Duration, SystemTime}, + time::{Duration, SystemTime, UNIX_EPOCH}, }; use tempfile::{NamedTempFile, TempDir}; use text::LineEnding; @@ -179,13 +180,62 @@ pub struct RemoveOptions { #[derive(Copy, Clone, Debug)] pub struct Metadata { pub inode: u64, - pub mtime: SystemTime, + pub mtime: MTime, pub is_symlink: bool, pub is_dir: bool, pub len: u64, pub is_fifo: bool, } +/// Filesystem modification time. The purpose of this newtype is to discourage use of operations +/// that do not make sense for mtimes. In particular, it is not always valid to compare mtimes using +/// `<` or `>`, as there are many things that can cause the mtime of a file to be earlier than it +/// was. See ["mtime comparison considered harmful" - apenwarr](https://apenwarr.ca/log/20181113). +/// +/// Do not derive Ord, PartialOrd, or arithmetic operation traits. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] +#[serde(transparent)] +pub struct MTime(SystemTime); + +impl MTime { + /// Conversion intended for persistence and testing. + pub fn from_seconds_and_nanos(secs: u64, nanos: u32) -> Self { + MTime(UNIX_EPOCH + Duration::new(secs, nanos)) + } + + /// Conversion intended for persistence. + pub fn to_seconds_and_nanos_for_persistence(self) -> Option<(u64, u32)> { + self.0 + .duration_since(UNIX_EPOCH) + .ok() + .map(|duration| (duration.as_secs(), duration.subsec_nanos())) + } + + /// Returns the value wrapped by this `MTime`, for presentation to the user. The name including + /// "_for_user" is to discourage misuse - this method should not be used when making decisions + /// about file dirtiness. + pub fn timestamp_for_user(self) -> SystemTime { + self.0 + } + + /// Temporary method to split out the behavior changes from introduction of this newtype. + pub fn bad_is_greater_than(self, other: MTime) -> bool { + self.0 > other.0 + } +} + +impl From for MTime { + fn from(timestamp: proto::Timestamp) -> Self { + MTime(timestamp.into()) + } +} + +impl From for proto::Timestamp { + fn from(mtime: MTime) -> Self { + mtime.0.into() + } +} + #[derive(Default)] pub struct RealFs { git_hosting_provider_registry: Arc, @@ -558,7 +608,7 @@ impl Fs for RealFs { Ok(Some(Metadata { inode, - mtime: metadata.modified().unwrap(), + mtime: MTime(metadata.modified().unwrap()), len: metadata.len(), is_symlink, is_dir: metadata.file_type().is_dir(), @@ -818,13 +868,13 @@ struct FakeFsState { enum FakeFsEntry { File { inode: u64, - mtime: SystemTime, + mtime: MTime, len: u64, content: Vec, }, Dir { inode: u64, - mtime: SystemTime, + mtime: MTime, len: u64, entries: BTreeMap>>, git_repo_state: Option>>, @@ -836,6 +886,18 @@ enum FakeFsEntry { #[cfg(any(test, feature = "test-support"))] impl FakeFsState { + fn get_and_increment_mtime(&mut self) -> MTime { + let mtime = self.next_mtime; + self.next_mtime += FakeFs::SYSTEMTIME_INTERVAL; + MTime(mtime) + } + + fn get_and_increment_inode(&mut self) -> u64 { + let inode = self.next_inode; + self.next_inode += 1; + inode + } + fn read_path(&self, target: &Path) -> Result>> { Ok(self .try_read_path(target, true) @@ -959,7 +1021,7 @@ pub static FS_DOT_GIT: std::sync::LazyLock<&'static OsStr> = impl FakeFs { /// We need to use something large enough for Windows and Unix to consider this a new file. /// https://doc.rust-lang.org/nightly/std/time/struct.SystemTime.html#platform-specific-behavior - const SYSTEMTIME_INTERVAL: u64 = 100; + const SYSTEMTIME_INTERVAL: Duration = Duration::from_nanos(100); pub fn new(executor: gpui::BackgroundExecutor) -> Arc { let (tx, mut rx) = smol::channel::bounded::(10); @@ -969,13 +1031,13 @@ impl FakeFs { state: Mutex::new(FakeFsState { root: Arc::new(Mutex::new(FakeFsEntry::Dir { inode: 0, - mtime: SystemTime::UNIX_EPOCH, + mtime: MTime(UNIX_EPOCH), len: 0, entries: Default::default(), git_repo_state: None, })), git_event_tx: tx, - next_mtime: SystemTime::UNIX_EPOCH, + next_mtime: UNIX_EPOCH + Self::SYSTEMTIME_INTERVAL, next_inode: 1, event_txs: Default::default(), buffered_events: Vec::new(), @@ -1007,13 +1069,16 @@ impl FakeFs { state.next_mtime = next_mtime; } + pub fn get_and_increment_mtime(&self) -> MTime { + let mut state = self.state.lock(); + state.get_and_increment_mtime() + } + pub async fn touch_path(&self, path: impl AsRef) { let mut state = self.state.lock(); let path = path.as_ref(); - let new_mtime = state.next_mtime; - let new_inode = state.next_inode; - state.next_inode += 1; - state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL); + let new_mtime = state.get_and_increment_mtime(); + let new_inode = state.get_and_increment_inode(); state .write_path(path, move |entry| { match entry { @@ -1062,19 +1127,14 @@ impl FakeFs { fn write_file_internal(&self, path: impl AsRef, content: Vec) -> Result<()> { let mut state = self.state.lock(); - let path = path.as_ref(); - let inode = state.next_inode; - let mtime = state.next_mtime; - state.next_inode += 1; - state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL); let file = Arc::new(Mutex::new(FakeFsEntry::File { - inode, - mtime, + inode: state.get_and_increment_inode(), + mtime: state.get_and_increment_mtime(), len: content.len() as u64, content, })); let mut kind = None; - state.write_path(path, { + state.write_path(path.as_ref(), { let kind = &mut kind; move |entry| { match entry { @@ -1090,7 +1150,7 @@ impl FakeFs { Ok(()) } })?; - state.emit_event([(path, kind)]); + state.emit_event([(path.as_ref(), kind)]); Ok(()) } @@ -1383,16 +1443,6 @@ impl FakeFsEntry { } } - fn set_file_content(&mut self, path: &Path, new_content: Vec) -> Result<()> { - if let Self::File { content, mtime, .. } = self { - *mtime = SystemTime::now(); - *content = new_content; - Ok(()) - } else { - Err(anyhow!("not a file: {}", path.display())) - } - } - fn dir_entries( &mut self, path: &Path, @@ -1456,10 +1506,8 @@ impl Fs for FakeFs { } let mut state = self.state.lock(); - let inode = state.next_inode; - let mtime = state.next_mtime; - state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL); - state.next_inode += 1; + let inode = state.get_and_increment_inode(); + let mtime = state.get_and_increment_mtime(); state.write_path(&cur_path, |entry| { entry.or_insert_with(|| { created_dirs.push((cur_path.clone(), Some(PathEventKind::Created))); @@ -1482,10 +1530,8 @@ impl Fs for FakeFs { async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> { self.simulate_random_delay().await; let mut state = self.state.lock(); - let inode = state.next_inode; - let mtime = state.next_mtime; - state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL); - state.next_inode += 1; + let inode = state.get_and_increment_inode(); + let mtime = state.get_and_increment_mtime(); let file = Arc::new(Mutex::new(FakeFsEntry::File { inode, mtime, @@ -1625,13 +1671,12 @@ impl Fs for FakeFs { let source = normalize_path(source); let target = normalize_path(target); let mut state = self.state.lock(); - let mtime = state.next_mtime; - let inode = util::post_inc(&mut state.next_inode); - state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL); + let mtime = state.get_and_increment_mtime(); + let inode = state.get_and_increment_inode(); let source_entry = state.read_path(&source)?; let content = source_entry.lock().file_content(&source)?.clone(); let mut kind = Some(PathEventKind::Created); - let entry = state.write_path(&target, |e| match e { + state.write_path(&target, |e| match e { btree_map::Entry::Occupied(e) => { if options.overwrite { kind = Some(PathEventKind::Changed); @@ -1647,14 +1692,11 @@ impl Fs for FakeFs { inode, mtime, len: content.len() as u64, - content: Vec::new(), + content, }))) .clone(), )), })?; - if let Some(entry) = entry { - entry.lock().set_file_content(&target, content)?; - } state.emit_event([(target, kind)]); Ok(()) } diff --git a/crates/git/Cargo.toml b/crates/git/Cargo.toml index 06a46b3b76..8723e41ce4 100644 --- a/crates/git/Cargo.toml +++ b/crates/git/Cargo.toml @@ -31,10 +31,6 @@ time.workspace = true url.workspace = true util.workspace = true -[target.'cfg(target_os = "windows")'.dependencies] -windows.workspace = true - - [dev-dependencies] unindent.workspace = true serde_json.workspace = true diff --git a/crates/git/src/blame.rs b/crates/git/src/blame.rs index 030309df96..8f87a8ca54 100644 --- a/crates/git/src/blame.rs +++ b/crates/git/src/blame.rs @@ -4,7 +4,7 @@ use anyhow::{anyhow, Context, Result}; use collections::{HashMap, HashSet}; use serde::{Deserialize, Serialize}; use std::io::Write; -use std::process::{Command, Stdio}; +use std::process::Stdio; use std::sync::Arc; use std::{ops::Range, path::Path}; use text::Rope; @@ -80,9 +80,7 @@ fn run_git_blame( path: &Path, contents: &Rope, ) -> Result { - let mut child = Command::new(git_binary); - - child + let child = util::command::new_std_command(git_binary) .current_dir(working_directory) .arg("blame") .arg("--incremental") @@ -91,15 +89,7 @@ fn run_git_blame( .arg(path.as_os_str()) .stdin(Stdio::piped()) .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - - #[cfg(windows)] - { - use std::os::windows::process::CommandExt; - child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); - } - - let child = child + .stderr(Stdio::piped()) .spawn() .map_err(|e| anyhow!("Failed to start git blame process: {}", e))?; diff --git a/crates/git/src/commit.rs b/crates/git/src/commit.rs index bdac6ff287..f32ad226af 100644 --- a/crates/git/src/commit.rs +++ b/crates/git/src/commit.rs @@ -2,10 +2,6 @@ use crate::Oid; use anyhow::{anyhow, Result}; use collections::HashMap; use std::path::Path; -use std::process::Command; - -#[cfg(windows)] -use std::os::windows::process::CommandExt; pub fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result> { if shas.is_empty() { @@ -14,19 +10,12 @@ pub fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result Result { - let mut child = Command::new(git_binary); - - child + let child = util::command::new_std_command(git_binary) .current_dir(working_directory) .args([ "--no-optional-locks", @@ -37,15 +35,7 @@ impl GitStatus { })) .stdin(Stdio::null()) .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - - #[cfg(windows)] - { - use std::os::windows::process::CommandExt; - child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); - } - - let child = child + .stderr(Stdio::piped()) .spawn() .map_err(|e| anyhow!("Failed to start git status process: {}", e))?; diff --git a/crates/gpui/README.md b/crates/gpui/README.md index 3ca0dcf7ca..6c0a5b607c 100644 --- a/crates/gpui/README.md +++ b/crates/gpui/README.md @@ -61,4 +61,4 @@ In addition to the systems above, GPUI provides a range of smaller services that - The `[gpui::test]` macro provides a convenient way to write tests for your GPUI applications. Tests also have their own kind of context, a `TestAppContext` which provides ways of simulating common platform input. See `app::test_context` and `test` modules for more details. -Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop a question in the [Zed Discord](https://discord.gg/zed-community). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog). +Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog). diff --git a/crates/gpui/examples/gif_viewer.rs b/crates/gpui/examples/gif_viewer.rs index 455a7d6ba9..da175e8a69 100644 --- a/crates/gpui/examples/gif_viewer.rs +++ b/crates/gpui/examples/gif_viewer.rs @@ -1,6 +1,4 @@ -use gpui::{ - div, img, prelude::*, App, AppContext, ImageSource, Render, ViewContext, WindowOptions, -}; +use gpui::{div, img, prelude::*, App, AppContext, Render, ViewContext, WindowOptions}; use std::path::PathBuf; struct GifViewer { @@ -16,7 +14,7 @@ impl GifViewer { impl Render for GifViewer { fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { div().size_full().child( - img(ImageSource::File(self.gif_path.clone().into())) + img(self.gif_path.clone()) .size_full() .object_fit(gpui::ObjectFit::Contain) .id("gif"), diff --git a/crates/gpui/examples/image/color.svg b/crates/gpui/examples/image/color.svg index 84e9809d09..a080681b0e 100644 --- a/crates/gpui/examples/image/color.svg +++ b/crates/gpui/examples/image/color.svg @@ -6,8 +6,8 @@ - + - - \ No newline at end of file + + diff --git a/crates/gpui/examples/image/image.rs b/crates/gpui/examples/image/image.rs index 24a94bf746..19a4e9313f 100644 --- a/crates/gpui/examples/image/image.rs +++ b/crates/gpui/examples/image/image.rs @@ -61,7 +61,7 @@ impl RenderOnce for ImageContainer { } struct ImageShowcase { - local_resource: Arc, + local_resource: Arc, remote_resource: SharedUri, asset_resource: SharedString, } @@ -153,9 +153,10 @@ fn main() { cx.open_window(window_options, |cx| { cx.new_view(|_cx| ImageShowcase { // Relative path to your root project path - local_resource: Arc::new( - PathBuf::from_str("crates/gpui/examples/image/app-icon.png").unwrap(), - ), + local_resource: PathBuf::from_str("crates/gpui/examples/image/app-icon.png") + .unwrap() + .into(), + remote_resource: "https://picsum.photos/512/512".into(), asset_resource: "image/color.svg".into(), diff --git a/crates/gpui/examples/image_loading.rs b/crates/gpui/examples/image_loading.rs new file mode 100644 index 0000000000..eef7d03e43 --- /dev/null +++ b/crates/gpui/examples/image_loading.rs @@ -0,0 +1,214 @@ +use std::{path::Path, sync::Arc, time::Duration}; + +use anyhow::anyhow; +use gpui::{ + black, div, img, prelude::*, pulsating_between, px, red, size, Animation, AnimationExt, App, + AppContext, Asset, AssetLogger, AssetSource, Bounds, Hsla, ImageAssetLoader, ImageCacheError, + ImgResourceLoader, Length, Pixels, RenderImage, Resource, SharedString, ViewContext, + WindowBounds, WindowContext, WindowOptions, LOADING_DELAY, +}; + +struct Assets {} + +impl AssetSource for Assets { + fn load(&self, path: &str) -> anyhow::Result>> { + std::fs::read(path) + .map(Into::into) + .map_err(Into::into) + .map(Some) + } + + fn list(&self, path: &str) -> anyhow::Result> { + Ok(std::fs::read_dir(path)? + .filter_map(|entry| { + Some(SharedString::from( + entry.ok()?.path().to_string_lossy().to_string(), + )) + }) + .collect::>()) + } +} + +const IMAGE: &str = "examples/image/app-icon.png"; + +#[derive(Copy, Clone, Hash)] +struct LoadImageParameters { + timeout: Duration, + fail: bool, +} + +struct LoadImageWithParameters {} + +impl Asset for LoadImageWithParameters { + type Source = LoadImageParameters; + + type Output = Result, ImageCacheError>; + + fn load( + parameters: Self::Source, + cx: &mut AppContext, + ) -> impl std::future::Future + Send + 'static { + let timer = cx.background_executor().timer(parameters.timeout); + let data = AssetLogger::::load( + Resource::Path(Path::new(IMAGE).to_path_buf().into()), + cx, + ); + async move { + timer.await; + if parameters.fail { + log::error!("Intentionally failed to load image"); + Err(anyhow!("Failed to load image").into()) + } else { + data.await + } + } + } +} + +struct ImageLoadingExample {} + +impl ImageLoadingExample { + fn loading_element() -> impl IntoElement { + div().size_full().flex_none().p_0p5().rounded_sm().child( + div().size_full().with_animation( + "loading-bg", + Animation::new(Duration::from_secs(3)) + .repeat() + .with_easing(pulsating_between(0.04, 0.24)), + move |this, delta| this.bg(black().opacity(delta)), + ), + ) + } + + fn fallback_element() -> impl IntoElement { + let fallback_color: Hsla = black().opacity(0.5); + + div().size_full().flex_none().p_0p5().child( + div() + .size_full() + .flex() + .items_center() + .justify_center() + .rounded_sm() + .text_sm() + .text_color(fallback_color) + .border_1() + .border_color(fallback_color) + .child("?"), + ) + } +} + +impl Render for ImageLoadingExample { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + div().flex().flex_col().size_full().justify_around().child( + div().flex().flex_row().w_full().justify_around().child( + div() + .flex() + .bg(gpui::white()) + .size(Length::Definite(Pixels(300.0).into())) + .justify_center() + .items_center() + .child({ + let image_source = LoadImageParameters { + timeout: LOADING_DELAY.saturating_sub(Duration::from_millis(25)), + fail: false, + }; + + // Load within the 'loading delay', should not show loading fallback + img(move |cx: &mut WindowContext| { + cx.use_asset::(&image_source) + }) + .id("image-1") + .border_1() + .size_12() + .with_fallback(|| Self::fallback_element().into_any_element()) + .border_color(red()) + .with_loading(|| Self::loading_element().into_any_element()) + .on_click(move |_, cx| { + cx.remove_asset::(&image_source); + }) + }) + .child({ + // Load after a long delay + let image_source = LoadImageParameters { + timeout: Duration::from_secs(5), + fail: false, + }; + + img(move |cx: &mut WindowContext| { + cx.use_asset::(&image_source) + }) + .id("image-2") + .with_fallback(|| Self::fallback_element().into_any_element()) + .with_loading(|| Self::loading_element().into_any_element()) + .size_12() + .border_1() + .border_color(red()) + .on_click(move |_, cx| { + cx.remove_asset::(&image_source); + }) + }) + .child({ + // Fail to load image after a long delay + let image_source = LoadImageParameters { + timeout: Duration::from_secs(5), + fail: true, + }; + + // Fail to load after a long delay + img(move |cx: &mut WindowContext| { + cx.use_asset::(&image_source) + }) + .id("image-3") + .with_fallback(|| Self::fallback_element().into_any_element()) + .with_loading(|| Self::loading_element().into_any_element()) + .size_12() + .border_1() + .border_color(red()) + .on_click(move |_, cx| { + cx.remove_asset::(&image_source); + }) + }) + .child({ + // Ensure that the normal image loader doesn't spam logs + let image_source = Path::new( + "this/file/really/shouldn't/exist/or/won't/be/an/image/I/hope", + ) + .to_path_buf(); + img(image_source.clone()) + .id("image-1") + .border_1() + .size_12() + .with_fallback(|| Self::fallback_element().into_any_element()) + .border_color(red()) + .with_loading(|| Self::loading_element().into_any_element()) + .on_click(move |_, cx| { + cx.remove_asset::(&image_source.clone().into()); + }) + }), + ), + ) + } +} + +fn main() { + env_logger::init(); + App::new() + .with_assets(Assets {}) + .run(|cx: &mut AppContext| { + let options = WindowOptions { + window_bounds: Some(WindowBounds::Windowed(Bounds::centered( + None, + size(px(300.), Pixels(300.)), + cx, + ))), + ..Default::default() + }; + cx.open_window(options, |cx| { + cx.activate(false); + cx.new_view(|_cx| ImageLoadingExample {}) + }) + .unwrap(); + }); +} diff --git a/crates/gpui/examples/input.rs b/crates/gpui/examples/input.rs index d52697c43f..1a49688a8f 100644 --- a/crates/gpui/examples/input.rs +++ b/crates/gpui/examples/input.rs @@ -15,7 +15,10 @@ actions!( SelectAll, Home, End, - ShowCharacterPalette + ShowCharacterPalette, + Paste, + Cut, + Copy, ] ); @@ -107,6 +110,28 @@ impl TextInput { cx.show_character_palette(); } + fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { + if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) { + self.replace_text_in_range(None, &text.replace("\n", " "), cx); + } + } + + fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { + if !self.selected_range.is_empty() { + cx.write_to_clipboard(ClipboardItem::new_string( + (&self.content[self.selected_range.clone()]).to_string(), + )); + } + } + fn cut(&mut self, _: &Copy, cx: &mut ViewContext) { + if !self.selected_range.is_empty() { + cx.write_to_clipboard(ClipboardItem::new_string( + (&self.content[self.selected_range.clone()]).to_string(), + )); + self.replace_text_in_range(None, "", cx) + } + } + fn move_to(&mut self, offset: usize, cx: &mut ViewContext) { self.selected_range = offset..offset; cx.notify() @@ -219,9 +244,11 @@ impl ViewInputHandler for TextInput { fn text_for_range( &mut self, range_utf16: Range, + actual_range: &mut Option>, _cx: &mut ViewContext, ) -> Option { let range = self.range_from_utf16(&range_utf16); + actual_range.replace(self.range_to_utf16(&range)); Some(self.content[range].to_string()) } @@ -497,6 +524,9 @@ impl Render for TextInput { .on_action(cx.listener(Self::home)) .on_action(cx.listener(Self::end)) .on_action(cx.listener(Self::show_character_palette)) + .on_action(cx.listener(Self::paste)) + .on_action(cx.listener(Self::cut)) + .on_action(cx.listener(Self::copy)) .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down)) .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up)) .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up)) @@ -581,8 +611,8 @@ impl Render for InputExample { format!( "{:} {}", ks.unparse(), - if let Some(ime_key) = ks.ime_key.as_ref() { - format!("-> {:?}", ime_key) + if let Some(key_char) = ks.key_char.as_ref() { + format!("-> {:?}", key_char) } else { "".to_owned() } @@ -602,6 +632,9 @@ fn main() { KeyBinding::new("shift-left", SelectLeft, None), KeyBinding::new("shift-right", SelectRight, None), KeyBinding::new("cmd-a", SelectAll, None), + KeyBinding::new("cmd-v", Paste, None), + KeyBinding::new("cmd-c", Copy, None), + KeyBinding::new("cmd-x", Cut, None), KeyBinding::new("home", Home, None), KeyBinding::new("end", End, None), KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None), diff --git a/crates/gpui/examples/painting.rs b/crates/gpui/examples/painting.rs new file mode 100644 index 0000000000..6e5fe25dfd --- /dev/null +++ b/crates/gpui/examples/painting.rs @@ -0,0 +1,199 @@ +use gpui::{ + canvas, div, point, prelude::*, px, size, App, AppContext, Bounds, MouseDownEvent, Path, + Pixels, Point, Render, ViewContext, WindowOptions, +}; +struct PaintingViewer { + default_lines: Vec>, + lines: Vec>>, + start: Point, + _painting: bool, +} + +impl PaintingViewer { + fn new() -> Self { + let mut lines = vec![]; + + // draw a line + let mut path = Path::new(point(px(50.), px(180.))); + path.line_to(point(px(100.), px(120.))); + // go back to close the path + path.line_to(point(px(100.), px(121.))); + path.line_to(point(px(50.), px(181.))); + lines.push(path); + + // draw a lightening bolt ⚡ + let mut path = Path::new(point(px(150.), px(200.))); + path.line_to(point(px(200.), px(125.))); + path.line_to(point(px(200.), px(175.))); + path.line_to(point(px(250.), px(100.))); + lines.push(path); + + // draw a ⭐ + let mut path = Path::new(point(px(350.), px(100.))); + path.line_to(point(px(370.), px(160.))); + path.line_to(point(px(430.), px(160.))); + path.line_to(point(px(380.), px(200.))); + path.line_to(point(px(400.), px(260.))); + path.line_to(point(px(350.), px(220.))); + path.line_to(point(px(300.), px(260.))); + path.line_to(point(px(320.), px(200.))); + path.line_to(point(px(270.), px(160.))); + path.line_to(point(px(330.), px(160.))); + path.line_to(point(px(350.), px(100.))); + lines.push(path); + + let square_bounds = Bounds { + origin: point(px(450.), px(100.)), + size: size(px(200.), px(80.)), + }; + let height = square_bounds.size.height; + let horizontal_offset = height; + let vertical_offset = px(30.); + let mut path = Path::new(square_bounds.lower_left()); + path.curve_to( + square_bounds.origin + point(horizontal_offset, vertical_offset), + square_bounds.origin + point(px(0.0), vertical_offset), + ); + path.line_to(square_bounds.upper_right() + point(-horizontal_offset, vertical_offset)); + path.curve_to( + square_bounds.lower_right(), + square_bounds.upper_right() + point(px(0.0), vertical_offset), + ); + path.line_to(square_bounds.lower_left()); + lines.push(path); + + Self { + default_lines: lines.clone(), + lines: vec![], + start: point(px(0.), px(0.)), + _painting: false, + } + } + + fn clear(&mut self, cx: &mut ViewContext) { + self.lines.clear(); + cx.notify(); + } +} +impl Render for PaintingViewer { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let default_lines = self.default_lines.clone(); + let lines = self.lines.clone(); + div() + .font_family(".SystemUIFont") + .bg(gpui::white()) + .size_full() + .p_4() + .flex() + .flex_col() + .child( + div() + .flex() + .gap_2() + .justify_between() + .items_center() + .child("Mouse down any point and drag to draw lines (Hold on shift key to draw straight lines)") + .child( + div() + .id("clear") + .child("Clean up") + .bg(gpui::black()) + .text_color(gpui::white()) + .active(|this| this.opacity(0.8)) + .flex() + .px_3() + .py_1() + .on_click(cx.listener(|this, _, cx| { + this.clear(cx); + })), + ), + ) + .child( + div() + .size_full() + .child( + canvas( + move |_, _| {}, + move |_, _, cx| { + const STROKE_WIDTH: Pixels = px(2.0); + for path in default_lines { + cx.paint_path(path, gpui::black()); + } + for points in lines { + let mut path = Path::new(points[0]); + for p in points.iter().skip(1) { + path.line_to(*p); + } + + let mut last = points.last().unwrap(); + for p in points.iter().rev() { + let mut offset_x = px(0.); + if last.x == p.x { + offset_x = STROKE_WIDTH; + } + path.line_to(point(p.x + offset_x, p.y + STROKE_WIDTH)); + last = p; + } + + cx.paint_path(path, gpui::black()); + } + }, + ) + .size_full(), + ) + .on_mouse_down( + gpui::MouseButton::Left, + cx.listener(|this, ev: &MouseDownEvent, _| { + this._painting = true; + this.start = ev.position; + let path = vec![ev.position]; + this.lines.push(path); + }), + ) + .on_mouse_move(cx.listener(|this, ev: &gpui::MouseMoveEvent, cx| { + if !this._painting { + return; + } + + let is_shifted = ev.modifiers.shift; + let mut pos = ev.position; + // When holding shift, draw a straight line + if is_shifted { + let dx = pos.x - this.start.x; + let dy = pos.y - this.start.y; + if dx.abs() > dy.abs() { + pos.y = this.start.y; + } else { + pos.x = this.start.x; + } + } + + if let Some(path) = this.lines.last_mut() { + path.push(pos); + } + + cx.notify(); + })) + .on_mouse_up( + gpui::MouseButton::Left, + cx.listener(|this, _, _| { + this._painting = false; + }), + ), + ) + } +} + +fn main() { + App::new().run(|cx: &mut AppContext| { + cx.open_window( + WindowOptions { + focus: true, + ..Default::default() + }, + |cx| cx.new_view(|_| PaintingViewer::new()), + ) + .unwrap(); + cx.activate(true); + }); +} diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 2a8da1c506..0776e5c72e 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -740,7 +740,7 @@ impl AppContext { } /// Returns the SVG renderer GPUI uses - pub(crate) fn svg_renderer(&self) -> SvgRenderer { + pub fn svg_renderer(&self) -> SvgRenderer { self.svg_renderer.clone() } @@ -1362,7 +1362,7 @@ impl AppContext { } /// Remove an asset from GPUI's cache - pub fn remove_cached_asset(&mut self, source: &A::Source) { + pub fn remove_asset(&mut self, source: &A::Source) { let asset_id = (TypeId::of::(), hash(source)); self.loading_assets.remove(&asset_id); } @@ -1371,12 +1371,7 @@ impl AppContext { /// /// Note that the multiple calls to this method will only result in one `Asset::load` call at a /// time, and the results of this call will be cached - /// - /// This asset will not be cached by default, see [Self::use_cached_asset] - pub fn fetch_asset( - &mut self, - source: &A::Source, - ) -> (Shared>, bool) { + pub fn fetch_asset(&mut self, source: &A::Source) -> (Shared>, bool) { let asset_id = (TypeId::of::(), hash(source)); let mut is_first = false; let task = self diff --git a/crates/gpui/src/asset_cache.rs b/crates/gpui/src/asset_cache.rs index 0c6e5f2f90..57ac1fb582 100644 --- a/crates/gpui/src/asset_cache.rs +++ b/crates/gpui/src/asset_cache.rs @@ -1,30 +1,43 @@ use crate::{AppContext, SharedString, SharedUri}; use futures::Future; + +use std::fmt::Debug; use std::hash::{Hash, Hasher}; -use std::path::PathBuf; +use std::marker::PhantomData; +use std::path::{Path, PathBuf}; use std::sync::Arc; +/// An enum representing #[derive(Debug, PartialEq, Eq, Hash, Clone)] -pub(crate) enum UriOrPath { +pub enum Resource { + /// This resource is at a given URI Uri(SharedUri), - Path(Arc), + /// This resource is at a given path in the file system + Path(Arc), + /// This resource is embedded in the application binary Embedded(SharedString), } -impl From for UriOrPath { +impl From for Resource { fn from(value: SharedUri) -> Self { Self::Uri(value) } } -impl From> for UriOrPath { - fn from(value: Arc) -> Self { +impl From for Resource { + fn from(value: PathBuf) -> Self { + Self::Path(value.into()) + } +} + +impl From> for Resource { + fn from(value: Arc) -> Self { Self::Path(value) } } /// A trait for asynchronous asset loading. -pub trait Asset { +pub trait Asset: 'static { /// The source of the asset. type Source: Clone + Hash + Send; @@ -38,6 +51,31 @@ pub trait Asset { ) -> impl Future + Send + 'static; } +/// An asset Loader that logs whatever passes through it +pub enum AssetLogger { + #[doc(hidden)] + _Phantom(PhantomData, &'static dyn crate::seal::Sealed), +} + +impl>> Asset + for AssetLogger +{ + type Source = T::Source; + + type Output = T::Output; + + fn load( + source: Self::Source, + cx: &mut AppContext, + ) -> impl Future + Send + 'static { + let load = T::load(source, cx); + async { + load.await + .inspect_err(|e| log::error!("Failed to load asset: {}", e)) + } + } +} + /// Use a quick, non-cryptographically secure hash function to get an identifier from data pub fn hash(data: &T) -> u64 { let mut hasher = collections::FxHasher::default(); diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 6a1f375b65..9c831d0875 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -22,6 +22,17 @@ pub fn rgba(hex: u32) -> Rgba { Rgba { r, g, b, a } } +/// Swap from RGBA with premultiplied alpha to BGRA +pub(crate) fn swap_rgba_pa_to_bgra(color: &mut [u8]) { + color.swap(0, 2); + if color[3] > 0 { + let a = color[3] as f32 / 255.; + color[0] = (color[0] as f32 / a) as u8; + color[1] = (color[1] as f32 / a) as u8; + color[2] = (color[2] as f32 / a) as u8; + } +} + /// An RGBA color #[derive(PartialEq, Clone, Copy, Default)] pub struct Rgba { diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 961e28364f..6928ca74ee 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -2383,7 +2383,7 @@ where /// A wrapper around an element that can store state, produced after assigning an ElementId. pub struct Stateful { - element: E, + pub(crate) element: E, } impl Styled for Stateful diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index 729e944b34..895904c801 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -1,9 +1,11 @@ use crate::{ - px, AbsoluteLength, AppContext, Asset, Bounds, DefiniteLength, Element, ElementId, - GlobalElementId, Hitbox, Image, InteractiveElement, Interactivity, IntoElement, LayoutId, - Length, ObjectFit, Pixels, RenderImage, SharedString, SharedUri, StyleRefinement, Styled, - SvgSize, UriOrPath, WindowContext, + px, swap_rgba_pa_to_bgra, AbsoluteLength, AnyElement, AppContext, Asset, AssetLogger, Bounds, + DefiniteLength, Element, ElementId, GlobalElementId, Hitbox, Image, InteractiveElement, + Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource, + SharedString, SharedUri, StyleRefinement, Styled, SvgSize, Task, WindowContext, }; +use anyhow::{anyhow, Result}; + use futures::{AsyncReadExt, Future}; use image::{ codecs::gif::GifDecoder, AnimationDecoder, Frame, ImageBuffer, ImageError, ImageFormat, @@ -11,45 +13,56 @@ use image::{ use smallvec::SmallVec; use std::{ fs, - io::Cursor, - path::PathBuf, + io::{self, Cursor}, + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, + str::FromStr, sync::Arc, time::{Duration, Instant}, }; use thiserror::Error; use util::ResultExt; +use super::{FocusableElement, Stateful, StatefulInteractiveElement}; + +/// The delay before showing the loading state. +pub const LOADING_DELAY: Duration = Duration::from_millis(200); + +/// A type alias to the resource loader that the `img()` element uses. +/// +/// Note: that this is only for Resources, like URLs or file paths. +/// Custom loaders, or external images will not use this asset loader +pub type ImgResourceLoader = AssetLogger; + /// A source of image content. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone)] pub enum ImageSource { - /// Image content will be loaded from provided URI at render time. - Uri(SharedUri), - /// Image content will be loaded from the provided file at render time. - File(Arc), + /// The image content will be loaded from some resource location + Resource(Resource), /// Cached image data Render(Arc), /// Cached image data Image(Arc), - /// Image content will be loaded from Asset at render time. - Embedded(SharedString), + /// A custom loading function to use + Custom(Arc Option, ImageCacheError>>>), } fn is_uri(uri: &str) -> bool { - uri.contains("://") + http_client::Uri::from_str(uri).is_ok() } impl From for ImageSource { fn from(value: SharedUri) -> Self { - Self::Uri(value) + Self::Resource(Resource::Uri(value)) } } -impl From<&'static str> for ImageSource { - fn from(s: &'static str) -> Self { +impl<'a> From<&'a str> for ImageSource { + fn from(s: &'a str) -> Self { if is_uri(s) { - Self::Uri(s.into()) + Self::Resource(Resource::Uri(s.to_string().into())) } else { - Self::Embedded(s.into()) + Self::Resource(Resource::Embedded(s.to_string().into())) } } } @@ -57,32 +70,34 @@ impl From<&'static str> for ImageSource { impl From for ImageSource { fn from(s: String) -> Self { if is_uri(&s) { - Self::Uri(s.into()) + Self::Resource(Resource::Uri(s.into())) } else { - Self::Embedded(s.into()) + Self::Resource(Resource::Embedded(s.into())) } } } impl From for ImageSource { fn from(s: SharedString) -> Self { - if is_uri(&s) { - Self::Uri(s.into()) - } else { - Self::Embedded(s) - } + s.as_ref().into() } } -impl From> for ImageSource { - fn from(value: Arc) -> Self { - Self::File(value) +impl From<&Path> for ImageSource { + fn from(value: &Path) -> Self { + Self::Resource(value.to_path_buf().into()) + } +} + +impl From> for ImageSource { + fn from(value: Arc) -> Self { + Self::Resource(value.into()) } } impl From for ImageSource { fn from(value: PathBuf) -> Self { - Self::File(value.into()) + Self::Resource(value.into()) } } @@ -98,12 +113,80 @@ impl From> for ImageSource { } } +impl Option, ImageCacheError>> + 'static> + From for ImageSource +{ + fn from(value: F) -> Self { + Self::Custom(Arc::new(value)) + } +} + +/// The style of an image element. +pub struct ImageStyle { + grayscale: bool, + object_fit: ObjectFit, + loading: Option AnyElement>>, + fallback: Option AnyElement>>, +} + +impl Default for ImageStyle { + fn default() -> Self { + Self { + grayscale: false, + object_fit: ObjectFit::Contain, + loading: None, + fallback: None, + } + } +} + +/// Style an image element. +pub trait StyledImage: Sized { + /// Get a mutable [ImageStyle] from the element. + fn image_style(&mut self) -> &mut ImageStyle; + + /// Set the image to be displayed in grayscale. + fn grayscale(mut self, grayscale: bool) -> Self { + self.image_style().grayscale = grayscale; + self + } + + /// Set the object fit for the image. + fn object_fit(mut self, object_fit: ObjectFit) -> Self { + self.image_style().object_fit = object_fit; + self + } + + /// Set the object fit for the image. + fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self { + self.image_style().fallback = Some(Box::new(fallback)); + self + } + + /// Set the object fit for the image. + fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self { + self.image_style().loading = Some(Box::new(loading)); + self + } +} + +impl StyledImage for Img { + fn image_style(&mut self) -> &mut ImageStyle { + &mut self.style + } +} + +impl StyledImage for Stateful { + fn image_style(&mut self) -> &mut ImageStyle { + &mut self.element.style + } +} + /// An image element. pub struct Img { interactivity: Interactivity, source: ImageSource, - grayscale: bool, - object_fit: ObjectFit, + style: ImageStyle, } /// Create a new image element. @@ -111,8 +194,7 @@ pub fn img(source: impl Into) -> Img { Img { interactivity: Interactivity::default(), source: source.into(), - grayscale: false, - object_fit: ObjectFit::Contain, + style: ImageStyle::default(), } } @@ -125,16 +207,19 @@ impl Img { "hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg", ] } +} - /// Set the image to be displayed in grayscale. - pub fn grayscale(mut self, grayscale: bool) -> Self { - self.grayscale = grayscale; - self +impl Deref for Stateful { + type Target = Img; + + fn deref(&self) -> &Self::Target { + &self.element } - /// Set the object fit for the image. - pub fn object_fit(mut self, object_fit: ObjectFit) -> Self { - self.object_fit = object_fit; - self +} + +impl DerefMut for Stateful { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.element } } @@ -142,10 +227,17 @@ impl Img { struct ImgState { frame_index: usize, last_frame_time: Option, + started_loading: Option<(Instant, Task<()>)>, +} + +/// The image layout state between frames +pub struct ImgLayoutState { + frame_index: usize, + replacement: Option, } impl Element for Img { - type RequestLayoutState = usize; + type RequestLayoutState = ImgLayoutState; type PrepaintState = Option; fn id(&self) -> Option { @@ -157,11 +249,17 @@ impl Element for Img { global_id: Option<&GlobalElementId>, cx: &mut WindowContext, ) -> (LayoutId, Self::RequestLayoutState) { + let mut layout_state = ImgLayoutState { + frame_index: 0, + replacement: None, + }; + cx.with_optional_element_state(global_id, |state, cx| { let mut state = state.map(|state| { state.unwrap_or(ImgState { frame_index: 0, last_frame_time: None, + started_loading: None, }) }); @@ -170,64 +268,105 @@ impl Element for Img { let layout_id = self .interactivity .request_layout(global_id, cx, |mut style, cx| { - if let Some(data) = self.source.use_data(cx) { - if let Some(state) = &mut state { - let frame_count = data.frame_count(); - if frame_count > 1 { - let current_time = Instant::now(); - if let Some(last_frame_time) = state.last_frame_time { - let elapsed = current_time - last_frame_time; - let frame_duration = - Duration::from(data.delay(state.frame_index)); + let mut replacement_id = None; - if elapsed >= frame_duration { - state.frame_index = (state.frame_index + 1) % frame_count; - state.last_frame_time = - Some(current_time - (elapsed - frame_duration)); + match self.source.use_data(cx) { + Some(Ok(data)) => { + if let Some(state) = &mut state { + let frame_count = data.frame_count(); + if frame_count > 1 { + let current_time = Instant::now(); + if let Some(last_frame_time) = state.last_frame_time { + let elapsed = current_time - last_frame_time; + let frame_duration = + Duration::from(data.delay(state.frame_index)); + + if elapsed >= frame_duration { + state.frame_index = + (state.frame_index + 1) % frame_count; + state.last_frame_time = + Some(current_time - (elapsed - frame_duration)); + } + } else { + state.last_frame_time = Some(current_time); + } + } + state.started_loading = None; + } + + let image_size = data.size(frame_index); + + if let Length::Auto = style.size.width { + style.size.width = match style.size.height { + Length::Definite(DefiniteLength::Absolute( + AbsoluteLength::Pixels(height), + )) => Length::Definite( + px(image_size.width.0 as f32 * height.0 + / image_size.height.0 as f32) + .into(), + ), + _ => Length::Definite(px(image_size.width.0 as f32).into()), + }; + } + + if let Length::Auto = style.size.height { + style.size.height = match style.size.width { + Length::Definite(DefiniteLength::Absolute( + AbsoluteLength::Pixels(width), + )) => Length::Definite( + px(image_size.height.0 as f32 * width.0 + / image_size.width.0 as f32) + .into(), + ), + _ => Length::Definite(px(image_size.height.0 as f32).into()), + }; + } + + if global_id.is_some() && data.frame_count() > 1 { + cx.request_animation_frame(); + } + } + Some(_err) => { + if let Some(fallback) = self.style.fallback.as_ref() { + let mut element = fallback(); + replacement_id = Some(element.request_layout(cx)); + layout_state.replacement = Some(element); + } + if let Some(state) = &mut state { + state.started_loading = None; + } + } + None => { + if let Some(state) = &mut state { + if let Some((started_loading, _)) = state.started_loading { + if started_loading.elapsed() > LOADING_DELAY { + if let Some(loading) = self.style.loading.as_ref() { + let mut element = loading(); + replacement_id = Some(element.request_layout(cx)); + layout_state.replacement = Some(element); + } } } else { - state.last_frame_time = Some(current_time); + let parent_view_id = cx.parent_view_id(); + let task = cx.spawn(|mut cx| async move { + cx.background_executor().timer(LOADING_DELAY).await; + cx.update(|cx| { + cx.notify(parent_view_id); + }) + .ok(); + }); + state.started_loading = Some((Instant::now(), task)); } } } - - let image_size = data.size(frame_index); - - if let Length::Auto = style.size.width { - style.size.width = match style.size.height { - Length::Definite(DefiniteLength::Absolute( - AbsoluteLength::Pixels(height), - )) => Length::Definite( - px(image_size.width.0 as f32 * height.0 - / image_size.height.0 as f32) - .into(), - ), - _ => Length::Definite(px(image_size.width.0 as f32).into()), - }; - } - - if let Length::Auto = style.size.height { - style.size.height = match style.size.width { - Length::Definite(DefiniteLength::Absolute( - AbsoluteLength::Pixels(width), - )) => Length::Definite( - px(image_size.height.0 as f32 * width.0 - / image_size.width.0 as f32) - .into(), - ), - _ => Length::Definite(px(image_size.height.0 as f32).into()), - }; - } - - if global_id.is_some() && data.frame_count() > 1 { - cx.request_animation_frame(); - } } - cx.request_layout(style, []) + cx.request_layout(style, replacement_id) }); - ((layout_id, frame_index), state) + layout_state.frame_index = frame_index; + + ((layout_id, layout_state), state) }) } @@ -235,18 +374,24 @@ impl Element for Img { &mut self, global_id: Option<&GlobalElementId>, bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, + request_layout: &mut Self::RequestLayoutState, cx: &mut WindowContext, - ) -> Option { + ) -> Self::PrepaintState { self.interactivity - .prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, _| hitbox) + .prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, cx| { + if let Some(replacement) = &mut request_layout.replacement { + replacement.prepaint(cx); + } + + hitbox + }) } fn paint( &mut self, global_id: Option<&GlobalElementId>, bounds: Bounds, - frame_index: &mut Self::RequestLayoutState, + layout_state: &mut Self::RequestLayoutState, hitbox: &mut Self::PrepaintState, cx: &mut WindowContext, ) { @@ -255,29 +400,26 @@ impl Element for Img { .paint(global_id, bounds, hitbox.as_ref(), cx, |style, cx| { let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size()); - if let Some(data) = source.use_data(cx) { - let new_bounds = self.object_fit.get_bounds(bounds, data.size(*frame_index)); + if let Some(Ok(data)) = source.use_data(cx) { + let new_bounds = self + .style + .object_fit + .get_bounds(bounds, data.size(layout_state.frame_index)); cx.paint_image( new_bounds, corner_radii, data.clone(), - *frame_index, - self.grayscale, + layout_state.frame_index, + self.style.grayscale, ) .log_err(); + } else if let Some(replacement) = &mut layout_state.replacement { + replacement.paint(cx); } }) } } -impl IntoElement for Img { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - impl Styled for Img { fn style(&mut self) -> &mut StyleRefinement { &mut self.interactivity.base_style @@ -290,41 +432,28 @@ impl InteractiveElement for Img { } } -impl ImageSource { - pub(crate) fn use_data(&self, cx: &mut WindowContext) -> Option> { - match self { - ImageSource::Uri(_) | ImageSource::Embedded(_) | ImageSource::File(_) => { - let uri_or_path: UriOrPath = match self { - ImageSource::Uri(uri) => uri.clone().into(), - ImageSource::File(path) => path.clone().into(), - ImageSource::Embedded(path) => UriOrPath::Embedded(path.clone()), - _ => unreachable!(), - }; +impl IntoElement for Img { + type Element = Self; - cx.use_asset::(&uri_or_path)?.log_err() - } - - ImageSource::Render(data) => Some(data.to_owned()), - ImageSource::Image(data) => cx.use_asset::(data)?.log_err(), - } + fn into_element(self) -> Self::Element { + self } +} - /// Fetch the data associated with this source, using GPUI's asset caching - pub async fn data(&self, cx: &mut AppContext) -> Option> { +impl FocusableElement for Img {} + +impl StatefulInteractiveElement for Img {} + +impl ImageSource { + pub(crate) fn use_data( + &self, + cx: &mut WindowContext, + ) -> Option, ImageCacheError>> { match self { - ImageSource::Uri(_) | ImageSource::Embedded(_) | ImageSource::File(_) => { - let uri_or_path: UriOrPath = match self { - ImageSource::Uri(uri) => uri.clone().into(), - ImageSource::File(path) => path.clone().into(), - ImageSource::Embedded(path) => UriOrPath::Embedded(path.clone()), - _ => unreachable!(), - }; - - cx.fetch_asset::(&uri_or_path).0.await.log_err() - } - - ImageSource::Render(data) => Some(data.to_owned()), - ImageSource::Image(data) => cx.fetch_asset::(data).0.await.log_err(), + ImageSource::Resource(resource) => cx.use_asset::(&resource), + ImageSource::Custom(loading_fn) => loading_fn(cx), + ImageSource::Render(data) => Some(Ok(data.to_owned())), + ImageSource::Image(data) => cx.use_asset::>(data), } } } @@ -334,22 +463,23 @@ enum ImageDecoder {} impl Asset for ImageDecoder { type Source = Arc; - type Output = Result, Arc>; + type Output = Result, ImageCacheError>; fn load( source: Self::Source, cx: &mut AppContext, ) -> impl Future + Send + 'static { - let result = source.to_image_data(cx).map_err(Arc::new); - async { result } + let renderer = cx.svg_renderer(); + async move { source.to_image_data(renderer).map_err(Into::into) } } } +/// An image loader for the GPUI asset system #[derive(Clone)] -enum ImageAsset {} +pub enum ImageAssetLoader {} -impl Asset for ImageAsset { - type Source = UriOrPath; +impl Asset for ImageAssetLoader { + type Source = Resource; type Output = Result, ImageCacheError>; fn load( @@ -363,12 +493,12 @@ impl Asset for ImageAsset { let asset_source = cx.asset_source().clone(); async move { let bytes = match source.clone() { - UriOrPath::Path(uri) => fs::read(uri.as_ref())?, - UriOrPath::Uri(uri) => { + Resource::Path(uri) => fs::read(uri.as_ref())?, + Resource::Uri(uri) => { let mut response = client .get(uri.as_ref(), ().into(), true) .await - .map_err(|e| ImageCacheError::Client(Arc::new(e)))?; + .map_err(|e| anyhow!(e))?; let mut body = Vec::new(); response.body_mut().read_to_end(&mut body).await?; if !response.status().is_success() { @@ -383,13 +513,13 @@ impl Asset for ImageAsset { } body } - UriOrPath::Embedded(path) => { + Resource::Embedded(path) => { let data = asset_source.load(&path).ok().flatten(); if let Some(data) = data { data.to_vec() } else { return Err(ImageCacheError::Asset( - format!("not found: {}", path).into(), + format!("Embedded resource not found: {}", path).into(), )); } } @@ -434,9 +564,8 @@ impl Asset for ImageAsset { let mut buffer = ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap(); - // Convert from RGBA to BGRA. for pixel in buffer.chunks_exact_mut(4) { - pixel.swap(0, 2); + swap_rgba_pa_to_bgra(pixel); } RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1)) @@ -450,9 +579,9 @@ impl Asset for ImageAsset { /// An error that can occur when interacting with the image cache. #[derive(Debug, Error, Clone)] pub enum ImageCacheError { - /// An error that occurred while fetching an image from a remote source. - #[error("http error: {0}")] - Client(#[from] Arc), + /// Some other kind of error occurred + #[error("error: {0}")] + Other(#[from] Arc), /// An error that occurred while reading the image from disk. #[error("IO error: {0}")] Io(Arc), @@ -477,20 +606,26 @@ pub enum ImageCacheError { Usvg(Arc), } -impl From for ImageCacheError { - fn from(error: std::io::Error) -> Self { - Self::Io(Arc::new(error)) +impl From for ImageCacheError { + fn from(value: anyhow::Error) -> Self { + Self::Other(Arc::new(value)) } } -impl From for ImageCacheError { - fn from(error: ImageError) -> Self { - Self::Image(Arc::new(error)) +impl From for ImageCacheError { + fn from(value: io::Error) -> Self { + Self::Io(Arc::new(value)) } } impl From for ImageCacheError { - fn from(error: usvg::Error) -> Self { - Self::Usvg(Arc::new(error)) + fn from(value: usvg::Error) -> Self { + Self::Usvg(Arc::new(value)) + } +} + +impl From for ImageCacheError { + fn from(value: image::ImageError) -> Self { + Self::Image(Arc::new(value)) } } diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 56b551737a..427097d1b7 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -263,7 +263,7 @@ impl TextLayout { .line_height .to_pixels(font_size.into(), cx.rem_size()); - let runs = if let Some(runs) = runs { + let mut runs = if let Some(runs) = runs { runs } else { vec![text_style.to_run(text.len())] @@ -306,7 +306,7 @@ impl TextLayout { let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size); let text = if let Some(truncate_width) = truncate_width { - line_wrapper.truncate_line(text.clone(), truncate_width, ellipsis) + line_wrapper.truncate_line(text.clone(), truncate_width, ellipsis, &mut runs) } else { text.clone() }; diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 2952f4af8a..51e2c3f173 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -56,7 +56,7 @@ //! and [`test`] modules for more details. //! //! Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop -//! a question in the [Zed Discord](https://discord.gg/zed-community). We're working on improving the documentation, creating more examples, +//! a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples, //! and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog). #![deny(missing_docs)] diff --git a/crates/gpui/src/input.rs b/crates/gpui/src/input.rs index 161401ecc6..2fb27ac7fc 100644 --- a/crates/gpui/src/input.rs +++ b/crates/gpui/src/input.rs @@ -9,8 +9,12 @@ use std::ops::Range; /// See [`InputHandler`] for details on how to implement each method. pub trait ViewInputHandler: 'static + Sized { /// See [`InputHandler::text_for_range`] for details - fn text_for_range(&mut self, range: Range, cx: &mut ViewContext) - -> Option; + fn text_for_range( + &mut self, + range: Range, + adjusted_range: &mut Option>, + cx: &mut ViewContext, + ) -> Option; /// See [`InputHandler::selected_text_range`] for details fn selected_text_range( @@ -89,10 +93,12 @@ impl InputHandler for ElementInputHandler { fn text_for_range( &mut self, range_utf16: Range, + adjusted_range: &mut Option>, cx: &mut WindowContext, ) -> Option { - self.view - .update(cx, |view, cx| view.text_for_range(range_utf16, cx)) + self.view.update(cx, |view, cx| { + view.text_for_range(range_utf16, adjusted_range, cx) + }) } fn replace_text_in_range( diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index a8424d197a..8228d44bb4 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -27,11 +27,11 @@ mod test; mod windows; use crate::{ - point, Action, AnyWindowHandle, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, - DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor, - GPUSpecs, GlyphId, ImageSource, Keymap, LineLayout, Pixels, PlatformInput, Point, - RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, ScaledPixels, Scene, - SharedString, Size, SvgSize, Task, TaskLabel, WindowContext, DEFAULT_WINDOW_SIZE, + point, Action, AnyWindowHandle, AsyncWindowContext, BackgroundExecutor, Bounds, DevicePixels, + DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GPUSpecs, GlyphId, + ImageSource, Keymap, LineLayout, Pixels, PlatformInput, Point, RenderGlyphParams, RenderImage, + RenderImageParams, RenderSvgParams, ScaledPixels, Scene, SharedString, Size, SvgRenderer, + SvgSize, Task, TaskLabel, WindowContext, DEFAULT_WINDOW_SIZE, }; use anyhow::{anyhow, Result}; use async_task::Runnable; @@ -46,6 +46,7 @@ use smallvec::SmallVec; use std::borrow::Cow; use std::hash::{Hash, Hasher}; use std::io::Cursor; +use std::ops; use std::time::{Duration, Instant}; use std::{ fmt::{self, Debug}, @@ -561,6 +562,42 @@ pub(crate) trait PlatformAtlas: Send + Sync { key: &AtlasKey, build: &mut dyn FnMut() -> Result, Cow<'a, [u8]>)>>, ) -> Result>; + fn remove(&self, key: &AtlasKey); +} + +struct AtlasTextureList { + textures: Vec>, + free_list: Vec, +} + +impl Default for AtlasTextureList { + fn default() -> Self { + Self { + textures: Vec::default(), + free_list: Vec::default(), + } + } +} + +impl ops::Index for AtlasTextureList { + type Output = Option; + + fn index(&self, index: usize) -> &Self::Output { + &self.textures[index] + } +} + +impl AtlasTextureList { + #[allow(unused)] + fn drain(&mut self) -> std::vec::Drain> { + self.free_list.clear(); + self.textures.drain(..) + } + + #[allow(dead_code)] + fn iter_mut(&mut self) -> impl DoubleEndedIterator { + self.textures.iter_mut().flatten() + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -643,9 +680,13 @@ impl PlatformInputHandler { } #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] - fn text_for_range(&mut self, range_utf16: Range) -> Option { + fn text_for_range( + &mut self, + range_utf16: Range, + adjusted: &mut Option>, + ) -> Option { self.cx - .update(|cx| self.handler.text_for_range(range_utf16, cx)) + .update(|cx| self.handler.text_for_range(range_utf16, adjusted, cx)) .ok() .flatten() } @@ -712,6 +753,7 @@ impl PlatformInputHandler { /// A struct representing a selection in a text buffer, in UTF16 characters. /// This is different from a range because the head may be before the tail. +#[derive(Debug)] pub struct UTF16Selection { /// The range of text in the document this selection corresponds to /// in UTF16 characters. @@ -749,6 +791,7 @@ pub trait InputHandler: 'static { fn text_for_range( &mut self, range_utf16: Range, + adjusted_range: &mut Option>, cx: &mut WindowContext, ) -> Option; @@ -1264,11 +1307,13 @@ impl Image { /// Use the GPUI `use_asset` API to make this image renderable pub fn use_render_image(self: Arc, cx: &mut WindowContext) -> Option> { - ImageSource::Image(self).use_data(cx) + ImageSource::Image(self) + .use_data(cx) + .and_then(|result| result.ok()) } /// Convert the clipboard image to an `ImageData` object. - pub fn to_image_data(&self, cx: &AppContext) -> Result> { + pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result> { fn frames_for_image( bytes: &[u8], format: image::ImageFormat, @@ -1305,10 +1350,7 @@ impl Image { ImageFormat::Bmp => frames_for_image(&self.bytes, image::ImageFormat::Bmp)?, ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?, ImageFormat::Svg => { - // TODO: Fix this - let pixmap = cx - .svg_renderer() - .render_pixmap(&self.bytes, SvgSize::ScaleFactor(1.0))?; + let pixmap = svg_renderer.render_pixmap(&self.bytes, SvgSize::ScaleFactor(1.0))?; let buffer = image::ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()) diff --git a/crates/gpui/src/platform/blade/blade_atlas.rs b/crates/gpui/src/platform/blade/blade_atlas.rs index e6d5dc8ee9..b876d5bb9b 100644 --- a/crates/gpui/src/platform/blade/blade_atlas.rs +++ b/crates/gpui/src/platform/blade/blade_atlas.rs @@ -1,6 +1,6 @@ use crate::{ - AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas, - Point, Size, + platform::AtlasTextureList, AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, + DevicePixels, PlatformAtlas, Point, Size, }; use anyhow::Result; use blade_graphics as gpu; @@ -67,7 +67,7 @@ impl BladeAtlas { pub(crate) fn clear_textures(&self, texture_kind: AtlasTextureKind) { let mut lock = self.0.lock(); let textures = &mut lock.storage[texture_kind]; - for texture in textures { + for texture in textures.iter_mut() { texture.clear(); } } @@ -130,19 +130,48 @@ impl PlatformAtlas for BladeAtlas { Ok(Some(tile)) } } + + fn remove(&self, key: &AtlasKey) { + let mut lock = self.0.lock(); + + let Some(id) = lock.tiles_by_key.remove(key).map(|tile| tile.texture_id) else { + return; + }; + + let Some(texture_slot) = lock.storage[id.kind].textures.get_mut(id.index as usize) else { + return; + }; + + if let Some(mut texture) = texture_slot.take() { + texture.decrement_ref_count(); + if texture.is_unreferenced() { + lock.storage[id.kind] + .free_list + .push(texture.id.index as usize); + texture.destroy(&lock.gpu); + } else { + *texture_slot = Some(texture); + } + } + } } impl BladeAtlasState { fn allocate(&mut self, size: Size, texture_kind: AtlasTextureKind) -> AtlasTile { - let textures = &mut self.storage[texture_kind]; - textures - .iter_mut() - .rev() - .find_map(|texture| texture.allocate(size)) - .unwrap_or_else(|| { - let texture = self.push_texture(size, texture_kind); - texture.allocate(size).unwrap() - }) + { + let textures = &mut self.storage[texture_kind]; + + if let Some(tile) = textures + .iter_mut() + .rev() + .find_map(|texture| texture.allocate(size)) + { + return tile; + } + } + + let texture = self.push_texture(size, texture_kind); + texture.allocate(size).unwrap() } fn push_texture( @@ -198,21 +227,30 @@ impl BladeAtlasState { }, ); - let textures = &mut self.storage[kind]; + let texture_list = &mut self.storage[kind]; + let index = texture_list.free_list.pop(); + let atlas_texture = BladeAtlasTexture { id: AtlasTextureId { - index: textures.len() as u32, + index: index.unwrap_or(texture_list.textures.len()) as u32, kind, }, allocator: etagere::BucketedAtlasAllocator::new(size.into()), format, raw, raw_view, + live_atlas_keys: 0, }; self.initializations.push(atlas_texture.id); - textures.push(atlas_texture); - textures.last_mut().unwrap() + + if let Some(ix) = index { + texture_list.textures[ix] = Some(atlas_texture); + texture_list.textures.get_mut(ix).unwrap().as_mut().unwrap() + } else { + texture_list.textures.push(Some(atlas_texture)); + texture_list.textures.last_mut().unwrap().as_mut().unwrap() + } } fn upload_texture(&mut self, id: AtlasTextureId, bounds: Bounds, bytes: &[u8]) { @@ -258,13 +296,13 @@ impl BladeAtlasState { #[derive(Default)] struct BladeAtlasStorage { - monochrome_textures: Vec, - polychrome_textures: Vec, - path_textures: Vec, + monochrome_textures: AtlasTextureList, + polychrome_textures: AtlasTextureList, + path_textures: AtlasTextureList, } impl ops::Index for BladeAtlasStorage { - type Output = Vec; + type Output = AtlasTextureList; fn index(&self, kind: AtlasTextureKind) -> &Self::Output { match kind { crate::AtlasTextureKind::Monochrome => &self.monochrome_textures, @@ -292,19 +330,19 @@ impl ops::Index for BladeAtlasStorage { crate::AtlasTextureKind::Polychrome => &self.polychrome_textures, crate::AtlasTextureKind::Path => &self.path_textures, }; - &textures[id.index as usize] + textures[id.index as usize].as_ref().unwrap() } } impl BladeAtlasStorage { fn destroy(&mut self, gpu: &gpu::Context) { - for mut texture in self.monochrome_textures.drain(..) { + for mut texture in self.monochrome_textures.drain().flatten() { texture.destroy(gpu); } - for mut texture in self.polychrome_textures.drain(..) { + for mut texture in self.polychrome_textures.drain().flatten() { texture.destroy(gpu); } - for mut texture in self.path_textures.drain(..) { + for mut texture in self.path_textures.drain().flatten() { texture.destroy(gpu); } } @@ -316,6 +354,7 @@ struct BladeAtlasTexture { raw: gpu::Texture, raw_view: gpu::TextureView, format: gpu::TextureFormat, + live_atlas_keys: u32, } impl BladeAtlasTexture { @@ -334,6 +373,7 @@ impl BladeAtlasTexture { size, }, }; + self.live_atlas_keys += 1; Some(tile) } @@ -345,6 +385,14 @@ impl BladeAtlasTexture { fn bytes_per_pixel(&self) -> u8 { self.format.block_info().size } + + fn decrement_ref_count(&mut self) { + self.live_atlas_keys -= 1; + } + + fn is_unreferenced(&mut self) -> bool { + self.live_atlas_keys == 0 + } } impl From> for etagere::Size { diff --git a/crates/gpui/src/platform/keystroke.rs b/crates/gpui/src/platform/keystroke.rs index f61beab9e9..af1e5179db 100644 --- a/crates/gpui/src/platform/keystroke.rs +++ b/crates/gpui/src/platform/keystroke.rs @@ -12,14 +12,15 @@ pub struct Keystroke { /// e.g. for option-s, key is "s" pub key: String, - /// ime_key is the character inserted by the IME engine when that key was pressed. - /// e.g. for option-s, ime_key is "ß" - pub ime_key: Option, + /// key_char is the character that could have been typed when + /// this binding was pressed. + /// e.g. for s this is "s", for option-s "ß", and cmd-s None + pub key_char: Option, } impl Keystroke { /// When matching a key we cannot know whether the user intended to type - /// the ime_key or the key itself. On some non-US keyboards keys we use in our + /// the key_char or the key itself. On some non-US keyboards keys we use in our /// bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard), /// and on some keyboards the IME handler converts a sequence of keys into a /// specific character (for example `"` is typed as `" space` on a brazilian keyboard). @@ -27,10 +28,10 @@ impl Keystroke { /// This method assumes that `self` was typed and `target' is in the keymap, and checks /// both possibilities for self against the target. pub(crate) fn should_match(&self, target: &Keystroke) -> bool { - if let Some(ime_key) = self - .ime_key + if let Some(key_char) = self + .key_char .as_ref() - .filter(|ime_key| ime_key != &&self.key) + .filter(|key_char| key_char != &&self.key) { let ime_modifiers = Modifiers { control: self.modifiers.control, @@ -38,7 +39,7 @@ impl Keystroke { ..Default::default() }; - if &target.key == ime_key && target.modifiers == ime_modifiers { + if &target.key == key_char && target.modifiers == ime_modifiers { return true; } } @@ -47,9 +48,9 @@ impl Keystroke { } /// key syntax is: - /// [ctrl-][alt-][shift-][cmd-][fn-]key[->ime_key] - /// ime_key syntax is only used for generating test events, - /// when matching a key with an ime_key set will be matched without it. + /// [ctrl-][alt-][shift-][cmd-][fn-]key[->key_char] + /// key_char syntax is only used for generating test events, + /// when matching a key with an key_char set will be matched without it. pub fn parse(source: &str) -> anyhow::Result { let mut control = false; let mut alt = false; @@ -57,7 +58,7 @@ impl Keystroke { let mut platform = false; let mut function = false; let mut key = None; - let mut ime_key = None; + let mut key_char = None; let mut components = source.split('-').peekable(); while let Some(component) = components.next() { @@ -74,7 +75,7 @@ impl Keystroke { break; } else if next.len() > 1 && next.starts_with('>') { key = Some(String::from(component)); - ime_key = Some(String::from(&next[1..])); + key_char = Some(String::from(&next[1..])); components.next(); } else { return Err(anyhow!("Invalid keystroke `{}`", source)); @@ -118,7 +119,7 @@ impl Keystroke { function, }, key, - ime_key, + key_char: key_char, }) } @@ -154,7 +155,7 @@ impl Keystroke { /// Returns true if this keystroke left /// the ime system in an incomplete state. pub fn is_ime_in_progress(&self) -> bool { - self.ime_key.is_none() + self.key_char.is_none() && (is_printable_key(&self.key) || self.key.is_empty()) && !(self.modifiers.platform || self.modifiers.control @@ -162,17 +163,17 @@ impl Keystroke { || self.modifiers.alt) } - /// Returns a new keystroke with the ime_key filled. + /// Returns a new keystroke with the key_char filled. /// This is used for dispatch_keystroke where we want users to /// be able to simulate typing "space", etc. pub fn with_simulated_ime(mut self) -> Self { - if self.ime_key.is_none() + if self.key_char.is_none() && !self.modifiers.platform && !self.modifiers.control && !self.modifiers.function && !self.modifiers.alt { - self.ime_key = match self.key.as_str() { + self.key_char = match self.key.as_str() { "space" => Some(" ".into()), "tab" => Some("\t".into()), "enter" => Some("\n".into()), @@ -222,6 +223,8 @@ fn is_printable_key(key: &str) -> bool { | "insert" | "home" | "end" + | "back" + | "forward" | "escape" ) } diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index a2e9af691b..650ed70af8 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -684,6 +684,8 @@ impl Keystroke { Keysym::ISO_Left_Tab => "tab".to_owned(), Keysym::KP_Prior => "pageup".to_owned(), Keysym::KP_Next => "pagedown".to_owned(), + Keysym::XF86_Back => "back".to_owned(), + Keysym::XF86_Forward => "forward".to_owned(), Keysym::comma => ",".to_owned(), Keysym::period => ".".to_owned(), @@ -740,14 +742,14 @@ impl Keystroke { } } - // Ignore control characters (and DEL) for the purposes of ime_key - let ime_key = + // Ignore control characters (and DEL) for the purposes of key_char + let key_char = (key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8); Keystroke { modifiers, key, - ime_key, + key_char, } } diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index ab87bb2024..e193201957 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -1208,7 +1208,7 @@ impl Dispatch for WaylandClientStatePtr { compose.feed(keysym); match compose.status() { xkb::Status::Composing => { - keystroke.ime_key = None; + keystroke.key_char = None; state.pre_edit_text = compose.utf8().or(Keystroke::underlying_dead_key(keysym)); let pre_edit = @@ -1220,7 +1220,7 @@ impl Dispatch for WaylandClientStatePtr { xkb::Status::Composed => { state.pre_edit_text.take(); - keystroke.ime_key = compose.utf8(); + keystroke.key_char = compose.utf8(); if let Some(keysym) = compose.keysym() { keystroke.key = xkb::keysym_get_name(keysym); } @@ -1340,7 +1340,7 @@ impl Dispatch for WaylandClientStatePtr { keystroke: Keystroke { modifiers: Modifiers::default(), key: commit_text.clone(), - ime_key: Some(commit_text), + key_char: Some(commit_text), }, is_held: false, })); diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 8d4516b3f3..55ba4f6004 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -687,11 +687,11 @@ impl WaylandWindowStatePtr { } } if let PlatformInput::KeyDown(event) = input { - if let Some(ime_key) = &event.keystroke.ime_key { + if let Some(key_char) = &event.keystroke.key_char { let mut state = self.state.borrow_mut(); if let Some(mut input_handler) = state.input_handler.take() { drop(state); - input_handler.replace_text_in_range(None, ime_key); + input_handler.replace_text_in_range(None, key_char); self.state.borrow_mut().input_handler = Some(input_handler); } } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 82ef39fc6b..1fd0e9aa66 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -178,7 +178,7 @@ pub struct X11ClientState { pub(crate) compose_state: Option, pub(crate) pre_edit_text: Option, pub(crate) composing: bool, - pub(crate) pre_ime_key_down: Option, + pub(crate) pre_key_char_down: Option, pub(crate) cursor_handle: cursor::Handle, pub(crate) cursor_styles: HashMap, pub(crate) cursor_cache: HashMap, @@ -446,7 +446,7 @@ impl X11Client { compose_state, pre_edit_text: None, - pre_ime_key_down: None, + pre_key_char_down: None, composing: false, cursor_handle, @@ -776,11 +776,11 @@ impl X11Client { }, }; let window = self.get_window(event.window)?; - window.configure(bounds); + window.configure(bounds).unwrap(); } Event::PropertyNotify(event) => { let window = self.get_window(event.window)?; - window.property_notify(event); + window.property_notify(event).unwrap(); } Event::FocusIn(event) => { let window = self.get_window(event.event)?; @@ -858,7 +858,7 @@ impl X11Client { let modifiers = modifiers_from_state(event.state); state.modifiers = modifiers; - state.pre_ime_key_down.take(); + state.pre_key_char_down.take(); let keystroke = { let code = event.detail.into(); let xkb_state = state.previous_xkb_state.clone(); @@ -880,13 +880,13 @@ impl X11Client { match compose_state.status() { xkbc::Status::Composed => { state.pre_edit_text.take(); - keystroke.ime_key = compose_state.utf8(); + keystroke.key_char = compose_state.utf8(); if let Some(keysym) = compose_state.keysym() { keystroke.key = xkbc::keysym_get_name(keysym); } } xkbc::Status::Composing => { - keystroke.ime_key = None; + keystroke.key_char = None; state.pre_edit_text = compose_state .utf8() .or(crate::Keystroke::underlying_dead_key(keysym)); @@ -1156,7 +1156,7 @@ impl X11Client { match event { Event::KeyPress(event) | Event::KeyRelease(event) => { let mut state = self.0.borrow_mut(); - state.pre_ime_key_down = Some(Keystroke::from_xkb( + state.pre_key_char_down = Some(Keystroke::from_xkb( &state.xkb, state.modifiers, event.detail.into(), @@ -1187,11 +1187,11 @@ impl X11Client { fn xim_handle_commit(&self, window: xproto::Window, text: String) -> Option<()> { let window = self.get_window(window).unwrap(); let mut state = self.0.borrow_mut(); - let keystroke = state.pre_ime_key_down.take(); + let keystroke = state.pre_key_char_down.take(); state.composing = false; drop(state); if let Some(mut keystroke) = keystroke { - keystroke.ime_key = Some(text.clone()); + keystroke.key_char = Some(text.clone()); window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent { keystroke, is_held: false, @@ -1258,11 +1258,9 @@ impl LinuxClient for X11Client { .iter() .enumerate() .filter_map(|(root_id, _)| { - Some(Rc::new(X11Display::new( - &state.xcb_connection, - state.scale_factor, - root_id, - )?) as Rc) + Some(Rc::new( + X11Display::new(&state.xcb_connection, state.scale_factor, root_id).ok()?, + ) as Rc) }) .collect() } @@ -1283,11 +1281,9 @@ impl LinuxClient for X11Client { fn display(&self, id: DisplayId) -> Option> { let state = self.0.borrow(); - Some(Rc::new(X11Display::new( - &state.xcb_connection, - state.scale_factor, - id.0 as usize, - )?)) + Some(Rc::new( + X11Display::new(&state.xcb_connection, state.scale_factor, id.0 as usize).ok()?, + )) } fn open_window( diff --git a/crates/gpui/src/platform/linux/x11/display.rs b/crates/gpui/src/platform/linux/x11/display.rs index 871d709fa9..4983e2f5a3 100644 --- a/crates/gpui/src/platform/linux/x11/display.rs +++ b/crates/gpui/src/platform/linux/x11/display.rs @@ -13,12 +13,17 @@ pub(crate) struct X11Display { impl X11Display { pub(crate) fn new( - xc: &XCBConnection, + xcb: &XCBConnection, scale_factor: f32, x_screen_index: usize, - ) -> Option { - let screen = xc.setup().roots.get(x_screen_index).unwrap(); - Some(Self { + ) -> anyhow::Result { + let Some(screen) = xcb.setup().roots.get(x_screen_index) else { + return Err(anyhow::anyhow!( + "No screen found with index {}", + x_screen_index + )); + }; + Ok(Self { x_screen_index, bounds: Bounds { origin: Default::default(), diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 15712233c2..ae9abe7146 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -1,4 +1,4 @@ -use anyhow::Context; +use anyhow::{anyhow, Context}; use crate::{ platform::blade::{BladeRenderer, BladeSurfaceConfig}, @@ -14,6 +14,8 @@ use raw_window_handle as rwh; use util::{maybe, ResultExt}; use x11rb::{ connection::Connection, + cookie::{Cookie, VoidCookie}, + errors::ConnectionError, properties::WmSizeHints, protocol::{ sync, @@ -25,7 +27,7 @@ use x11rb::{ }; use std::{ - cell::RefCell, ffi::c_void, mem::size_of, num::NonZeroU32, ops::Div, ptr::NonNull, rc::Rc, + cell::RefCell, ffi::c_void, fmt::Display, num::NonZeroU32, ops::Div, ptr::NonNull, rc::Rc, sync::Arc, }; @@ -77,17 +79,16 @@ x11rb::atom_manager! { } } -fn query_render_extent(xcb_connection: &XCBConnection, x_window: xproto::Window) -> gpu::Extent { - let reply = xcb_connection - .get_geometry(x_window) - .unwrap() - .reply() - .unwrap(); - gpu::Extent { +fn query_render_extent( + xcb: &Rc, + x_window: xproto::Window, +) -> anyhow::Result { + let reply = get_reply(|| "X11 GetGeometry failed.", xcb.get_geometry(x_window))?; + Ok(gpu::Extent { width: reply.width as u32, height: reply.height as u32, depth: 1, - } + }) } impl ResizeEdge { @@ -148,7 +149,7 @@ impl EdgeConstraints { } } -#[derive(Debug)] +#[derive(Copy, Clone, Debug)] struct Visual { id: xproto::Visualid, colormap: u32, @@ -163,8 +164,8 @@ struct VisualSet { black_pixel: u32, } -fn find_visuals(xcb_connection: &XCBConnection, screen_index: usize) -> VisualSet { - let screen = &xcb_connection.setup().roots[screen_index]; +fn find_visuals(xcb: &XCBConnection, screen_index: usize) -> VisualSet { + let screen = &xcb.setup().roots[screen_index]; let mut set = VisualSet { inherit: Visual { id: screen.root_visual, @@ -277,13 +278,16 @@ impl X11WindowState { pub(crate) struct X11WindowStatePtr { pub state: Rc>, pub(crate) callbacks: Rc>, - xcb_connection: Rc, + xcb: Rc, x_window: xproto::Window, } impl rwh::HasWindowHandle for RawWindow { fn window_handle(&self) -> Result { - let non_zero = NonZeroU32::new(self.window_id).unwrap(); + let Some(non_zero) = NonZeroU32::new(self.window_id) else { + log::error!("RawWindow.window_id zero when getting window handle."); + return Err(rwh::HandleError::Unavailable); + }; let mut handle = rwh::XcbWindowHandle::new(non_zero); handle.visual_id = NonZeroU32::new(self.visual_id); Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) }) @@ -291,7 +295,10 @@ impl rwh::HasWindowHandle for RawWindow { } impl rwh::HasDisplayHandle for RawWindow { fn display_handle(&self) -> Result { - let non_zero = NonNull::new(self.connection).unwrap(); + let Some(non_zero) = NonNull::new(self.connection) else { + log::error!("Null RawWindow.connection when getting display handle."); + return Err(rwh::HandleError::Unavailable); + }; let handle = rwh::XcbDisplayHandle::new(Some(non_zero), self.screen_id as i32); Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) } @@ -308,6 +315,43 @@ impl rwh::HasDisplayHandle for X11Window { } } +fn check_reply( + failure_context: F, + result: Result>, ConnectionError>, +) -> anyhow::Result<()> +where + C: Display + Send + Sync + 'static, + F: FnOnce() -> C, +{ + result + .map_err(|connection_error| anyhow!(connection_error)) + .and_then(|response| { + response + .check() + .map_err(|error_response| anyhow!(error_response)) + }) + .with_context(failure_context) +} + +fn get_reply( + failure_context: F, + result: Result, O>, ConnectionError>, +) -> anyhow::Result +where + C: Display + Send + Sync + 'static, + F: FnOnce() -> C, + O: x11rb::x11_utils::TryParse, +{ + result + .map_err(|connection_error| anyhow!(connection_error)) + .and_then(|response| { + response + .reply() + .map_err(|error_response| anyhow!(error_response)) + }) + .with_context(failure_context) +} + impl X11WindowState { #[allow(clippy::too_many_arguments)] pub fn new( @@ -315,7 +359,7 @@ impl X11WindowState { client: X11ClientStatePtr, executor: ForegroundExecutor, params: WindowParams, - xcb_connection: &Rc, + xcb: &Rc, client_side_decorations_supported: bool, x_main_screen_index: usize, x_window: xproto::Window, @@ -327,7 +371,7 @@ impl X11WindowState { .display_id .map_or(x_main_screen_index, |did| did.0 as usize); - let visual_set = find_visuals(&xcb_connection, x_screen_index); + let visual_set = find_visuals(&xcb, x_screen_index); let visual = match visual_set.transparent { Some(visual) => visual, @@ -341,12 +385,12 @@ impl X11WindowState { let colormap = if visual.colormap != 0 { visual.colormap } else { - let id = xcb_connection.generate_id().unwrap(); + let id = xcb.generate_id()?; log::info!("Creating colormap {}", id); - xcb_connection - .create_colormap(xproto::ColormapAlloc::NONE, id, visual_set.root, visual.id) - .unwrap() - .check()?; + check_reply( + || format!("X11 CreateColormap failed. id: {}", id), + xcb.create_colormap(xproto::ColormapAlloc::NONE, id, visual_set.root, visual.id), + )?; id }; @@ -370,8 +414,12 @@ impl X11WindowState { bounds.size.height = 600.into(); } - xcb_connection - .create_window( + check_reply( + || { + format!("X11 CreateWindow failed. depth: {}, x_window: {}, visual_set.root: {}, bounds.origin.x.0: {}, bounds.origin.y.0: {}, bounds.size.width.0: {}, bounds.size.height.0: {}", + visual.depth, x_window, visual_set.root, bounds.origin.x.0 + 2, bounds.origin.y.0, bounds.size.width.0, bounds.size.height.0) + }, + xcb.create_window( visual.depth, x_window, visual_set.root, @@ -383,189 +431,205 @@ impl X11WindowState { xproto::WindowClass::INPUT_OUTPUT, visual.id, &win_aux, - ) - .unwrap() - .check().with_context(|| { - format!("CreateWindow request to X server failed. depth: {}, x_window: {}, visual_set.root: {}, bounds.origin.x.0: {}, bounds.origin.y.0: {}, bounds.size.width.0: {}, bounds.size.height.0: {}", - visual.depth, x_window, visual_set.root, bounds.origin.x.0 + 2, bounds.origin.y.0, bounds.size.width.0, bounds.size.height.0) - })?; + ), + )?; - if let Some(size) = params.window_min_size { - let mut size_hints = WmSizeHints::new(); - size_hints.min_size = Some((size.width.0 as i32, size.height.0 as i32)); - size_hints - .set_normal_hints(xcb_connection, x_window) - .unwrap(); - } + // Collect errors during setup, so that window can be destroyed on failure. + let setup_result = maybe!({ + if let Some(size) = params.window_min_size { + let mut size_hints = WmSizeHints::new(); + let min_size = (size.width.0 as i32, size.height.0 as i32); + size_hints.min_size = Some(min_size); + check_reply( + || { + format!( + "X11 change of WM_SIZE_HINTS failed. min_size: {:?}", + min_size + ) + }, + size_hints.set_normal_hints(xcb, x_window), + )?; + } - let reply = xcb_connection - .get_geometry(x_window) - .unwrap() - .reply() - .unwrap(); - if reply.x == 0 && reply.y == 0 { - bounds.origin.x.0 += 2; - // Work around a bug where our rendered content appears - // outside the window bounds when opened at the default position - // (14px, 49px on X + Gnome + Ubuntu 22). - xcb_connection - .configure_window( - x_window, - &xproto::ConfigureWindowAux::new() - .x(bounds.origin.x.0) - .y(bounds.origin.y.0), - ) - .unwrap(); - } - if let Some(titlebar) = params.titlebar { - if let Some(title) = titlebar.title { - xcb_connection - .change_property8( + let reply = get_reply(|| "X11 GetGeometry failed.", xcb.get_geometry(x_window))?; + if reply.x == 0 && reply.y == 0 { + bounds.origin.x.0 += 2; + // Work around a bug where our rendered content appears + // outside the window bounds when opened at the default position + // (14px, 49px on X + Gnome + Ubuntu 22). + let x = bounds.origin.x.0; + let y = bounds.origin.y.0; + check_reply( + || format!("X11 ConfigureWindow failed. x: {}, y: {}", x, y), + xcb.configure_window(x_window, &xproto::ConfigureWindowAux::new().x(x).y(y)), + )?; + } + if let Some(titlebar) = params.titlebar { + if let Some(title) = titlebar.title { + check_reply( + || "X11 ChangeProperty8 on window title failed.", + xcb.change_property8( + xproto::PropMode::REPLACE, + x_window, + xproto::AtomEnum::WM_NAME, + xproto::AtomEnum::STRING, + title.as_bytes(), + ), + )?; + } + } + if params.kind == WindowKind::PopUp { + check_reply( + || "X11 ChangeProperty32 setting window type for pop-up failed.", + xcb.change_property32( xproto::PropMode::REPLACE, x_window, - xproto::AtomEnum::WM_NAME, - xproto::AtomEnum::STRING, - title.as_bytes(), - ) - .unwrap(); + atoms._NET_WM_WINDOW_TYPE, + xproto::AtomEnum::ATOM, + &[atoms._NET_WM_WINDOW_TYPE_NOTIFICATION], + ), + )?; } - } - if params.kind == WindowKind::PopUp { - xcb_connection - .change_property32( + + check_reply( + || "X11 ChangeProperty32 setting protocols failed.", + xcb.change_property32( xproto::PropMode::REPLACE, x_window, - atoms._NET_WM_WINDOW_TYPE, + atoms.WM_PROTOCOLS, xproto::AtomEnum::ATOM, - &[atoms._NET_WM_WINDOW_TYPE_NOTIFICATION], - ) - .unwrap(); + &[atoms.WM_DELETE_WINDOW, atoms._NET_WM_SYNC_REQUEST], + ), + )?; + + get_reply( + || "X11 sync protocol initialize failed.", + sync::initialize(xcb, 3, 1), + )?; + let sync_request_counter = xcb.generate_id()?; + check_reply( + || "X11 sync CreateCounter failed.", + sync::create_counter(xcb, sync_request_counter, sync::Int64 { lo: 0, hi: 0 }), + )?; + + check_reply( + || "X11 ChangeProperty32 setting sync request counter failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms._NET_WM_SYNC_REQUEST_COUNTER, + xproto::AtomEnum::CARDINAL, + &[sync_request_counter], + ), + )?; + + check_reply( + || "X11 XiSelectEvents failed.", + xcb.xinput_xi_select_events( + x_window, + &[xinput::EventMask { + deviceid: XINPUT_ALL_DEVICE_GROUPS, + mask: vec![ + xinput::XIEventMask::MOTION + | xinput::XIEventMask::BUTTON_PRESS + | xinput::XIEventMask::BUTTON_RELEASE + | xinput::XIEventMask::ENTER + | xinput::XIEventMask::LEAVE, + ], + }], + ), + )?; + + check_reply( + || "X11 XiSelectEvents for device changes failed.", + xcb.xinput_xi_select_events( + x_window, + &[xinput::EventMask { + deviceid: XINPUT_ALL_DEVICES, + mask: vec![ + xinput::XIEventMask::HIERARCHY | xinput::XIEventMask::DEVICE_CHANGED, + ], + }], + ), + )?; + + xcb.flush().with_context(|| "X11 Flush failed.")?; + + let raw = RawWindow { + connection: as_raw_xcb_connection::AsRawXcbConnection::as_raw_xcb_connection(xcb) + as *mut _, + screen_id: x_screen_index, + window_id: x_window, + visual_id: visual.id, + }; + let gpu = Arc::new( + unsafe { + gpu::Context::init_windowed( + &raw, + gpu::ContextDesc { + validation: false, + capture: false, + overlay: false, + }, + ) + } + .map_err(|e| anyhow!("{:?}", e))?, + ); + + let config = BladeSurfaceConfig { + // Note: this has to be done after the GPU init, or otherwise + // the sizes are immediately invalidated. + size: query_render_extent(xcb, x_window)?, + // We set it to transparent by default, even if we have client-side + // decorations, since those seem to work on X11 even without `true` here. + // If the window appearance changes, then the renderer will get updated + // too + transparent: false, + }; + check_reply(|| "X11 MapWindow failed.", xcb.map_window(x_window))?; + + let display = Rc::new(X11Display::new(xcb, scale_factor, x_screen_index)?); + + Ok(Self { + client, + executor, + display, + _raw: raw, + x_root_window: visual_set.root, + bounds: bounds.to_pixels(scale_factor), + scale_factor, + renderer: BladeRenderer::new(gpu, config), + atoms: *atoms, + input_handler: None, + active: false, + hovered: false, + fullscreen: false, + maximized_vertical: false, + maximized_horizontal: false, + hidden: false, + appearance, + handle, + background_appearance: WindowBackgroundAppearance::Opaque, + destroyed: false, + client_side_decorations_supported, + decorations: WindowDecorations::Server, + last_insets: [0, 0, 0, 0], + edge_constraints: None, + counter_id: sync_request_counter, + last_sync_counter: None, + }) + }); + + if setup_result.is_err() { + check_reply( + || "X11 DestroyWindow failed while cleaning it up after setup failure.", + xcb.destroy_window(x_window), + )?; + xcb.flush() + .with_context(|| "X11 Flush failed while cleaning it up after setup failure.")?; } - xcb_connection - .change_property32( - xproto::PropMode::REPLACE, - x_window, - atoms.WM_PROTOCOLS, - xproto::AtomEnum::ATOM, - &[atoms.WM_DELETE_WINDOW, atoms._NET_WM_SYNC_REQUEST], - ) - .unwrap(); - - sync::initialize(xcb_connection, 3, 1).unwrap(); - let sync_request_counter = xcb_connection.generate_id().unwrap(); - sync::create_counter( - xcb_connection, - sync_request_counter, - sync::Int64 { lo: 0, hi: 0 }, - ) - .unwrap(); - - xcb_connection - .change_property32( - xproto::PropMode::REPLACE, - x_window, - atoms._NET_WM_SYNC_REQUEST_COUNTER, - xproto::AtomEnum::CARDINAL, - &[sync_request_counter], - ) - .unwrap(); - - xcb_connection - .xinput_xi_select_events( - x_window, - &[xinput::EventMask { - deviceid: XINPUT_ALL_DEVICE_GROUPS, - mask: vec![ - xinput::XIEventMask::MOTION - | xinput::XIEventMask::BUTTON_PRESS - | xinput::XIEventMask::BUTTON_RELEASE - | xinput::XIEventMask::ENTER - | xinput::XIEventMask::LEAVE, - ], - }], - ) - .unwrap(); - - xcb_connection - .xinput_xi_select_events( - x_window, - &[xinput::EventMask { - deviceid: XINPUT_ALL_DEVICES, - mask: vec![ - xinput::XIEventMask::HIERARCHY, - xinput::XIEventMask::DEVICE_CHANGED, - ], - }], - ) - .unwrap(); - - xcb_connection.flush().unwrap(); - - let raw = RawWindow { - connection: as_raw_xcb_connection::AsRawXcbConnection::as_raw_xcb_connection( - xcb_connection, - ) as *mut _, - screen_id: x_screen_index, - window_id: x_window, - visual_id: visual.id, - }; - let gpu = Arc::new( - unsafe { - gpu::Context::init_windowed( - &raw, - gpu::ContextDesc { - validation: false, - capture: false, - overlay: false, - }, - ) - } - .map_err(|e| anyhow::anyhow!("{:?}", e))?, - ); - - let config = BladeSurfaceConfig { - // Note: this has to be done after the GPU init, or otherwise - // the sizes are immediately invalidated. - size: query_render_extent(xcb_connection, x_window), - // We set it to transparent by default, even if we have client-side - // decorations, since those seem to work on X11 even without `true` here. - // If the window appearance changes, then the renderer will get updated - // too - transparent: false, - }; - xcb_connection.map_window(x_window).unwrap(); - - Ok(Self { - client, - executor, - display: Rc::new( - X11Display::new(xcb_connection, scale_factor, x_screen_index).unwrap(), - ), - _raw: raw, - x_root_window: visual_set.root, - bounds: bounds.to_pixels(scale_factor), - scale_factor, - renderer: BladeRenderer::new(gpu, config), - atoms: *atoms, - input_handler: None, - active: false, - hovered: false, - fullscreen: false, - maximized_vertical: false, - maximized_horizontal: false, - hidden: false, - appearance, - handle, - background_appearance: WindowBackgroundAppearance::Opaque, - destroyed: false, - client_side_decorations_supported, - decorations: WindowDecorations::Server, - last_insets: [0, 0, 0, 0], - edge_constraints: None, - counter_id: sync_request_counter, - last_sync_counter: None, - }) + setup_result } fn content_size(&self) -> Size { @@ -577,6 +641,28 @@ impl X11WindowState { } } +/// A handle to an X11 window which destroys it on Drop. +pub struct X11WindowHandle { + id: xproto::Window, + xcb: Rc, +} + +impl Drop for X11WindowHandle { + fn drop(&mut self) { + maybe!({ + check_reply( + || "X11 DestroyWindow failed while dropping X11WindowHandle.", + self.xcb.destroy_window(self.id), + )?; + self.xcb + .flush() + .with_context(|| "X11 Flush failed while dropping X11WindowHandle.")?; + anyhow::Ok(()) + }) + .log_err(); + } +} + pub(crate) struct X11Window(pub X11WindowStatePtr); impl Drop for X11Window { @@ -585,13 +671,17 @@ impl Drop for X11Window { state.renderer.destroy(); let destroy_x_window = maybe!({ - self.0.xcb_connection.unmap_window(self.0.x_window)?; - self.0.xcb_connection.destroy_window(self.0.x_window)?; - self.0.xcb_connection.flush()?; + check_reply( + || "X11 DestroyWindow failure.", + self.0.xcb.destroy_window(self.0.x_window), + )?; + self.0 + .xcb + .flush() + .with_context(|| "X11 Flush failed after calling DestroyWindow.")?; anyhow::Ok(()) }) - .context("unmapping and destroying X11 window") .log_err(); if destroy_x_window.is_some() { @@ -627,7 +717,7 @@ impl X11Window { client: X11ClientStatePtr, executor: ForegroundExecutor, params: WindowParams, - xcb_connection: &Rc, + xcb: &Rc, client_side_decorations_supported: bool, x_main_screen_index: usize, x_window: xproto::Window, @@ -641,7 +731,7 @@ impl X11Window { client, executor, params, - xcb_connection, + xcb, client_side_decorations_supported, x_main_screen_index, x_window, @@ -650,17 +740,23 @@ impl X11Window { appearance, )?)), callbacks: Rc::new(RefCell::new(Callbacks::default())), - xcb_connection: xcb_connection.clone(), + xcb: xcb.clone(), x_window, }; let state = ptr.state.borrow_mut(); - ptr.set_wm_properties(state); + ptr.set_wm_properties(state)?; Ok(Self(ptr)) } - fn set_wm_hints(&self, wm_hint_property_state: WmHintPropertyState, prop1: u32, prop2: u32) { + fn set_wm_hints C>( + &self, + failure_context: F, + wm_hint_property_state: WmHintPropertyState, + prop1: u32, + prop2: u32, + ) -> anyhow::Result<()> { let state = self.0.state.borrow(); let message = ClientMessageEvent::new( 32, @@ -668,51 +764,45 @@ impl X11Window { state.atoms._NET_WM_STATE, [wm_hint_property_state as u32, prop1, prop2, 1, 0], ); - self.0 - .xcb_connection - .send_event( + check_reply( + failure_context, + self.0.xcb.send_event( false, state.x_root_window, EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY, message, - ) - .unwrap() - .check() - .unwrap(); + ), + ) } - fn get_root_position(&self, position: Point) -> TranslateCoordinatesReply { + fn get_root_position( + &self, + position: Point, + ) -> anyhow::Result { let state = self.0.state.borrow(); - self.0 - .xcb_connection - .translate_coordinates( + get_reply( + || "X11 TranslateCoordinates failed.", + self.0.xcb.translate_coordinates( self.0.x_window, state.x_root_window, (position.x.0 * state.scale_factor) as i16, (position.y.0 * state.scale_factor) as i16, - ) - .unwrap() - .reply() - .unwrap() + ), + ) } - fn send_moveresize(&self, flag: u32) { + fn send_moveresize(&self, flag: u32) -> anyhow::Result<()> { let state = self.0.state.borrow(); - self.0 - .xcb_connection - .ungrab_pointer(x11rb::CURRENT_TIME) - .unwrap() - .check() - .unwrap(); + check_reply( + || "X11 UngrabPointer before move/resize of window ailed.", + self.0.xcb.ungrab_pointer(x11rb::CURRENT_TIME), + )?; - let pointer = self - .0 - .xcb_connection - .query_pointer(self.0.x_window) - .unwrap() - .reply() - .unwrap(); + let pointer = get_reply( + || "X11 QueryPointer before move/resize of window failed.", + self.0.xcb.query_pointer(self.0.x_window), + )?; let message = ClientMessageEvent::new( 32, self.0.x_window, @@ -725,17 +815,21 @@ impl X11Window { 0, ], ); - self.0 - .xcb_connection - .send_event( + check_reply( + || "X11 SendEvent to move/resize window failed.", + self.0.xcb.send_event( false, state.x_root_window, EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY, message, - ) - .unwrap(); + ), + )?; - self.0.xcb_connection.flush().unwrap(); + self.flush() + } + + fn flush(&self) -> anyhow::Result<()> { + self.0.xcb.flush().with_context(|| "X11 Flush failed.") } } @@ -751,51 +845,56 @@ impl X11WindowStatePtr { } } - pub fn property_notify(&self, event: xproto::PropertyNotifyEvent) { + pub fn property_notify(&self, event: xproto::PropertyNotifyEvent) -> anyhow::Result<()> { let mut state = self.state.borrow_mut(); if event.atom == state.atoms._NET_WM_STATE { - self.set_wm_properties(state); + self.set_wm_properties(state)?; } else if event.atom == state.atoms._GTK_EDGE_CONSTRAINTS { - self.set_edge_constraints(state); + self.set_edge_constraints(state)?; } + Ok(()) } - fn set_edge_constraints(&self, mut state: std::cell::RefMut) { - let reply = self - .xcb_connection - .get_property( + fn set_edge_constraints( + &self, + mut state: std::cell::RefMut, + ) -> anyhow::Result<()> { + let reply = get_reply( + || "X11 GetProperty for _GTK_EDGE_CONSTRAINTS failed.", + self.xcb.get_property( false, self.x_window, state.atoms._GTK_EDGE_CONSTRAINTS, xproto::AtomEnum::CARDINAL, 0, 4, - ) - .unwrap() - .reply() - .unwrap(); + ), + )?; if reply.value_len != 0 { let atom = u32::from_ne_bytes(reply.value[0..4].try_into().unwrap()); let edge_constraints = EdgeConstraints::from_atom(atom); state.edge_constraints.replace(edge_constraints); } + + Ok(()) } - fn set_wm_properties(&self, mut state: std::cell::RefMut) { - let reply = self - .xcb_connection - .get_property( + fn set_wm_properties( + &self, + mut state: std::cell::RefMut, + ) -> anyhow::Result<()> { + let reply = get_reply( + || "X11 GetProperty for _NET_WM_STATE failed.", + self.xcb.get_property( false, self.x_window, state.atoms._NET_WM_STATE, xproto::AtomEnum::ATOM, 0, u32::MAX, - ) - .unwrap() - .reply() - .unwrap(); + ), + )?; let atoms = reply .value @@ -821,6 +920,8 @@ impl X11WindowStatePtr { state.hidden = true; } } + + Ok(()) } pub fn close(&self) { @@ -846,9 +947,9 @@ impl X11WindowStatePtr { if let PlatformInput::KeyDown(event) = input { let mut state = self.state.borrow_mut(); if let Some(mut input_handler) = state.input_handler.take() { - if let Some(ime_key) = &event.keystroke.ime_key { + if let Some(key_char) = &event.keystroke.key_char { drop(state); - input_handler.replace_text_in_range(None, ime_key); + input_handler.replace_text_in_range(None, key_char); state = self.state.borrow_mut(); } state.input_handler = Some(input_handler); @@ -912,7 +1013,7 @@ impl X11WindowStatePtr { bounds } - pub fn configure(&self, bounds: Bounds) { + pub fn configure(&self, bounds: Bounds) -> anyhow::Result<()> { let mut resize_args = None; let is_resize; { @@ -930,7 +1031,7 @@ impl X11WindowStatePtr { state.bounds = bounds; } - let gpu_size = query_render_extent(&self.xcb_connection, self.x_window); + let gpu_size = query_render_extent(&self.xcb, self.x_window)?; if true { state.renderer.update_drawable_size(size( DevicePixels(gpu_size.width as i32), @@ -939,7 +1040,10 @@ impl X11WindowStatePtr { resize_args = Some((state.content_size(), state.scale_factor)); } if let Some(value) = state.last_sync_counter.take() { - sync::set_counter(&self.xcb_connection, state.counter_id, value).unwrap(); + check_reply( + || "X11 sync SetCounter failed.", + sync::set_counter(&self.xcb, state.counter_id, value), + )?; } } @@ -951,9 +1055,11 @@ impl X11WindowStatePtr { } if !is_resize { if let Some(ref mut fun) = callbacks.moved { - fun() + fun(); } } + + Ok(()) } pub fn set_active(&self, focus: bool) { @@ -1025,13 +1131,11 @@ impl PlatformWindow for X11Window { } fn mouse_position(&self) -> Point { - let reply = self - .0 - .xcb_connection - .query_pointer(self.0.x_window) - .unwrap() - .reply() - .unwrap(); + let reply = get_reply( + || "X11 QueryPointer failed.", + self.0.xcb.query_pointer(self.0.x_window), + ) + .unwrap(); Point::new((reply.root_x as u32).into(), (reply.root_y as u32).into()) } @@ -1073,7 +1177,7 @@ impl PlatformWindow for X11Window { data, ); self.0 - .xcb_connection + .xcb .send_event( false, self.0.state.borrow().x_root_window, @@ -1082,14 +1186,14 @@ impl PlatformWindow for X11Window { ) .log_err(); self.0 - .xcb_connection + .xcb .set_input_focus( xproto::InputFocus::POINTER_ROOT, self.0.x_window, xproto::Time::CURRENT_TIME, ) .log_err(); - self.0.xcb_connection.flush().unwrap(); + self.flush().unwrap(); } fn is_active(&self) -> bool { @@ -1101,28 +1205,30 @@ impl PlatformWindow for X11Window { } fn set_title(&mut self, title: &str) { - self.0 - .xcb_connection - .change_property8( + check_reply( + || "X11 ChangeProperty8 on WM_NAME failed.", + self.0.xcb.change_property8( xproto::PropMode::REPLACE, self.0.x_window, xproto::AtomEnum::WM_NAME, xproto::AtomEnum::STRING, title.as_bytes(), - ) - .unwrap(); + ), + ) + .unwrap(); - self.0 - .xcb_connection - .change_property8( + check_reply( + || "X11 ChangeProperty8 on _NET_WM_NAME failed.", + self.0.xcb.change_property8( xproto::PropMode::REPLACE, self.0.x_window, self.0.state.borrow().atoms._NET_WM_NAME, self.0.state.borrow().atoms.UTF8_STRING, title.as_bytes(), - ) - .unwrap(); - self.0.xcb_connection.flush().unwrap(); + ), + ) + .unwrap(); + self.flush().unwrap(); } fn set_app_id(&mut self, app_id: &str) { @@ -1131,18 +1237,17 @@ impl PlatformWindow for X11Window { data.push(b'\0'); data.extend(app_id.bytes()); // class - self.0 - .xcb_connection - .change_property8( + check_reply( + || "X11 ChangeProperty8 for WM_CLASS failed.", + self.0.xcb.change_property8( xproto::PropMode::REPLACE, self.0.x_window, xproto::AtomEnum::WM_CLASS, xproto::AtomEnum::STRING, &data, - ) - .unwrap() - .check() - .unwrap(); + ), + ) + .unwrap(); } fn set_edited(&mut self, _edited: bool) { @@ -1169,35 +1274,38 @@ impl PlatformWindow for X11Window { state.atoms.WM_CHANGE_STATE, [WINDOW_ICONIC_STATE, 0, 0, 0, 0], ); - self.0 - .xcb_connection - .send_event( + check_reply( + || "X11 SendEvent to minimize window failed.", + self.0.xcb.send_event( false, state.x_root_window, EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY, message, - ) - .unwrap() - .check() - .unwrap(); + ), + ) + .unwrap(); } fn zoom(&self) { let state = self.0.state.borrow(); self.set_wm_hints( + || "X11 SendEvent to maximize a window failed.", WmHintPropertyState::Toggle, state.atoms._NET_WM_STATE_MAXIMIZED_VERT, state.atoms._NET_WM_STATE_MAXIMIZED_HORZ, - ); + ) + .unwrap(); } fn toggle_fullscreen(&self) { let state = self.0.state.borrow(); self.set_wm_hints( + || "X11 SendEvent to fullscreen a window failed.", WmHintPropertyState::Toggle, state.atoms._NET_WM_STATE_FULLSCREEN, xproto::AtomEnum::NONE.into(), - ); + ) + .unwrap(); } fn is_fullscreen(&self) -> bool { @@ -1253,14 +1361,13 @@ impl PlatformWindow for X11Window { fn show_window_menu(&self, position: Point) { let state = self.0.state.borrow(); - self.0 - .xcb_connection - .ungrab_pointer(x11rb::CURRENT_TIME) - .unwrap() - .check() - .unwrap(); + check_reply( + || "X11 UngrabPointer failed.", + self.0.xcb.ungrab_pointer(x11rb::CURRENT_TIME), + ) + .unwrap(); - let coords = self.get_root_position(position); + let coords = self.get_root_position(position).unwrap(); let message = ClientMessageEvent::new( 32, self.0.x_window, @@ -1273,26 +1380,25 @@ impl PlatformWindow for X11Window { 0, ], ); - self.0 - .xcb_connection - .send_event( + check_reply( + || "X11 SendEvent to show window menu failed.", + self.0.xcb.send_event( false, state.x_root_window, EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY, message, - ) - .unwrap() - .check() - .unwrap(); + ), + ) + .unwrap(); } fn start_window_move(&self) { const MOVERESIZE_MOVE: u32 = 8; - self.send_moveresize(MOVERESIZE_MOVE); + self.send_moveresize(MOVERESIZE_MOVE).unwrap(); } fn start_window_resize(&self, edge: ResizeEdge) { - self.send_moveresize(edge.to_moveresize()); + self.send_moveresize(edge.to_moveresize()).unwrap(); } fn window_decorations(&self) -> crate::Decorations { @@ -1355,9 +1461,9 @@ impl PlatformWindow for X11Window { if state.last_insets != insets { state.last_insets = insets; - self.0 - .xcb_connection - .change_property( + check_reply( + || "X11 ChangeProperty for _GTK_FRAME_EXTENTS failed.", + self.0.xcb.change_property( xproto::PropMode::REPLACE, self.0.x_window, state.atoms._GTK_FRAME_EXTENTS, @@ -1365,10 +1471,9 @@ impl PlatformWindow for X11Window { size_of::() as u8 * 8, 4, bytemuck::cast_slice::(&insets), - ) - .unwrap() - .check() - .unwrap(); + ), + ) + .unwrap(); } } @@ -1390,20 +1495,19 @@ impl PlatformWindow for X11Window { WindowDecorations::Client => [1 << 1, 0, 0, 0, 0], }; - self.0 - .xcb_connection - .change_property( + check_reply( + || "X11 ChangeProperty for _MOTIF_WM_HINTS failed.", + self.0.xcb.change_property( xproto::PropMode::REPLACE, self.0.x_window, state.atoms._MOTIF_WM_HINTS, state.atoms._MOTIF_WM_HINTS, - std::mem::size_of::() as u8 * 8, + size_of::() as u8 * 8, 5, bytemuck::cast_slice::(&hints_data), - ) - .unwrap() - .check() - .unwrap(); + ), + ) + .unwrap(); match decorations { WindowDecorations::Server => { diff --git a/crates/gpui/src/platform/mac/events.rs b/crates/gpui/src/platform/mac/events.rs index aeff08ada8..e1aae9db39 100644 --- a/crates/gpui/src/platform/mac/events.rs +++ b/crates/gpui/src/platform/mac/events.rs @@ -245,7 +245,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { .charactersIgnoringModifiers() .to_str() .to_string(); - let mut ime_key = None; + let mut key_char = None; let first_char = characters.chars().next().map(|ch| ch as u16); let modifiers = native_event.modifierFlags(); @@ -260,11 +260,20 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { #[allow(non_upper_case_globals)] let key = match first_char { - Some(SPACE_KEY) => "space".to_string(), + Some(SPACE_KEY) => { + key_char = Some(" ".to_string()); + "space".to_string() + } + Some(TAB_KEY) => { + key_char = Some("\t".to_string()); + "tab".to_string() + } + Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => { + key_char = Some("\n".to_string()); + "enter".to_string() + } Some(BACKSPACE_KEY) => "backspace".to_string(), - Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter".to_string(), Some(ESCAPE_KEY) => "escape".to_string(), - Some(TAB_KEY) => "tab".to_string(), Some(SHIFT_TAB_KEY) => "tab".to_string(), Some(NSUpArrowFunctionKey) => "up".to_string(), Some(NSDownArrowFunctionKey) => "down".to_string(), @@ -332,6 +341,18 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { chars_ignoring_modifiers = chars_with_cmd; } + if !control && !command && !function { + let mut mods = NO_MOD; + if shift { + mods |= SHIFT_MOD; + } + if alt { + mods |= OPTION_MOD; + } + + key_char = Some(chars_for_modified_key(native_event.keyCode(), mods)); + } + let mut key = if shift && chars_ignoring_modifiers .chars() @@ -345,20 +366,6 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { chars_ignoring_modifiers }; - if always_use_cmd_layout || alt { - let mut mods = NO_MOD; - if shift { - mods |= SHIFT_MOD; - } - if alt { - mods |= OPTION_MOD; - } - let alt_key = chars_for_modified_key(native_event.keyCode(), mods); - if alt_key != key { - ime_key = Some(alt_key); - } - }; - key } }; @@ -372,7 +379,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { function, }, key, - ime_key, + key_char, } } diff --git a/crates/gpui/src/platform/mac/metal_atlas.rs b/crates/gpui/src/platform/mac/metal_atlas.rs index 89a6987752..ca595c5ce3 100644 --- a/crates/gpui/src/platform/mac/metal_atlas.rs +++ b/crates/gpui/src/platform/mac/metal_atlas.rs @@ -1,6 +1,6 @@ use crate::{ - AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas, - Point, Size, + platform::AtlasTextureList, AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, + DevicePixels, PlatformAtlas, Point, Size, }; use anyhow::{anyhow, Result}; use collections::FxHashMap; @@ -42,7 +42,7 @@ impl MetalAtlas { AtlasTextureKind::Polychrome => &mut lock.polychrome_textures, AtlasTextureKind::Path => &mut lock.path_textures, }; - for texture in textures { + for texture in textures.iter_mut() { texture.clear(); } } @@ -50,9 +50,9 @@ impl MetalAtlas { struct MetalAtlasState { device: AssertSend, - monochrome_textures: Vec, - polychrome_textures: Vec, - path_textures: Vec, + monochrome_textures: AtlasTextureList, + polychrome_textures: AtlasTextureList, + path_textures: AtlasTextureList, tiles_by_key: FxHashMap, } @@ -78,6 +78,38 @@ impl PlatformAtlas for MetalAtlas { Ok(Some(tile)) } } + + fn remove(&self, key: &AtlasKey) { + let mut lock = self.0.lock(); + let Some(id) = lock.tiles_by_key.get(key).map(|v| v.texture_id) else { + return; + }; + + let textures = match id.kind { + AtlasTextureKind::Monochrome => &mut lock.monochrome_textures, + AtlasTextureKind::Polychrome => &mut lock.polychrome_textures, + AtlasTextureKind::Path => &mut lock.polychrome_textures, + }; + + let Some(texture_slot) = textures + .textures + .iter_mut() + .find(|texture| texture.as_ref().is_some_and(|v| v.id == id)) + else { + return; + }; + + if let Some(mut texture) = texture_slot.take() { + texture.decrement_ref_count(); + + if texture.is_unreferenced() { + textures.free_list.push(id.index as usize); + lock.tiles_by_key.remove(key); + } else { + *texture_slot = Some(texture); + } + } + } } impl MetalAtlasState { @@ -86,20 +118,24 @@ impl MetalAtlasState { size: Size, texture_kind: AtlasTextureKind, ) -> Option { - let textures = match texture_kind { - AtlasTextureKind::Monochrome => &mut self.monochrome_textures, - AtlasTextureKind::Polychrome => &mut self.polychrome_textures, - AtlasTextureKind::Path => &mut self.path_textures, - }; + { + let textures = match texture_kind { + AtlasTextureKind::Monochrome => &mut self.monochrome_textures, + AtlasTextureKind::Polychrome => &mut self.polychrome_textures, + AtlasTextureKind::Path => &mut self.path_textures, + }; - textures - .iter_mut() - .rev() - .find_map(|texture| texture.allocate(size)) - .or_else(|| { - let texture = self.push_texture(size, texture_kind); - texture.allocate(size) - }) + if let Some(tile) = textures + .iter_mut() + .rev() + .find_map(|texture| texture.allocate(size)) + { + return Some(tile); + } + } + + let texture = self.push_texture(size, texture_kind); + texture.allocate(size) } fn push_texture( @@ -140,21 +176,31 @@ impl MetalAtlasState { texture_descriptor.set_usage(usage); let metal_texture = self.device.new_texture(&texture_descriptor); - let textures = match kind { + let texture_list = match kind { AtlasTextureKind::Monochrome => &mut self.monochrome_textures, AtlasTextureKind::Polychrome => &mut self.polychrome_textures, AtlasTextureKind::Path => &mut self.path_textures, }; + + let index = texture_list.free_list.pop(); + let atlas_texture = MetalAtlasTexture { id: AtlasTextureId { - index: textures.len() as u32, + index: index.unwrap_or(texture_list.textures.len()) as u32, kind, }, allocator: etagere::BucketedAtlasAllocator::new(size.into()), metal_texture: AssertSend(metal_texture), + live_atlas_keys: 0, }; - textures.push(atlas_texture); - textures.last_mut().unwrap() + + if let Some(ix) = index { + texture_list.textures[ix] = Some(atlas_texture); + texture_list.textures.get_mut(ix).unwrap().as_mut().unwrap() + } else { + texture_list.textures.push(Some(atlas_texture)); + texture_list.textures.last_mut().unwrap().as_mut().unwrap() + } } fn texture(&self, id: AtlasTextureId) -> &MetalAtlasTexture { @@ -163,7 +209,7 @@ impl MetalAtlasState { crate::AtlasTextureKind::Polychrome => &self.polychrome_textures, crate::AtlasTextureKind::Path => &self.path_textures, }; - &textures[id.index as usize] + textures[id.index as usize].as_ref().unwrap() } } @@ -171,6 +217,7 @@ struct MetalAtlasTexture { id: AtlasTextureId, allocator: BucketedAtlasAllocator, metal_texture: AssertSend, + live_atlas_keys: u32, } impl MetalAtlasTexture { @@ -189,6 +236,7 @@ impl MetalAtlasTexture { }, padding: 0, }; + self.live_atlas_keys += 1; Some(tile) } @@ -215,6 +263,14 @@ impl MetalAtlasTexture { _ => unimplemented!(), } } + + fn decrement_ref_count(&mut self) { + self.live_atlas_keys -= 1; + } + + fn is_unreferenced(&mut self) -> bool { + self.live_atlas_keys == 0 + } } impl From> for etagere::Size { diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index b744c658ce..28f427af1b 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -343,8 +343,10 @@ impl MacPlatform { ns_string(key_to_native(&keystroke.key).as_ref()), ) .autorelease(); - let _: () = - msg_send![item, setAllowsAutomaticKeyEquivalentLocalization: NO]; + if MacPlatform::os_version().unwrap() >= SemanticVersion::new(12, 0, 0) { + let _: () = + msg_send![item, setAllowsAutomaticKeyEquivalentLocalization: NO]; + } item.setKeyEquivalentModifierMask_(mask); } // For multi-keystroke bindings, render the keystroke as part of the title. @@ -842,7 +844,9 @@ impl Platform for MacPlatform { let app: id = msg_send![APP_CLASS, sharedApplication]; let mut state = self.0.lock(); let actions = &mut state.menu_actions; - app.setMainMenu_(self.create_menu_bar(menus, NSWindow::delegate(app), actions, keymap)); + let menu = self.create_menu_bar(menus, NSWindow::delegate(app), actions, keymap); + drop(state); + app.setMainMenu_(menu); } } diff --git a/crates/gpui/src/platform/mac/text_system.rs b/crates/gpui/src/platform/mac/text_system.rs index 3db1bf9bcd..560c78ffb2 100644 --- a/crates/gpui/src/platform/mac/text_system.rs +++ b/crates/gpui/src/platform/mac/text_system.rs @@ -1,7 +1,8 @@ use crate::{ - point, px, size, Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, FontId, FontMetrics, - FontRun, FontStyle, FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, - RenderGlyphParams, Result, ShapedGlyph, ShapedRun, SharedString, Size, SUBPIXEL_VARIANTS, + point, px, size, swap_rgba_pa_to_bgra, Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, + FontId, FontMetrics, FontRun, FontStyle, FontWeight, GlyphId, LineLayout, Pixels, + PlatformTextSystem, Point, RenderGlyphParams, Result, ShapedGlyph, ShapedRun, SharedString, + Size, SUBPIXEL_VARIANTS, }; use anyhow::anyhow; use cocoa::appkit::CGFloat; @@ -418,11 +419,7 @@ impl MacTextSystemState { if params.is_emoji { // Convert from RGBA with premultiplied alpha to BGRA with straight alpha. for pixel in bytes.chunks_exact_mut(4) { - pixel.swap(0, 2); - let a = pixel[3] as f32 / 255.; - pixel[0] = (pixel[0] as f32 / a) as u8; - pixel[1] = (pixel[1] as f32 / a) as u8; - pixel[2] = (pixel[2] as f32 / a) as u8; + swap_rgba_pa_to_bgra(pixel); } } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index e5a04191a3..ce9a4c05bf 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -38,6 +38,7 @@ use std::{ cell::Cell, ffi::{c_void, CStr}, mem, + ops::Range, path::PathBuf, ptr::{self, NonNull}, rc::Rc, @@ -1283,18 +1284,17 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: } if event.is_held { - let handled = with_input_handler(&this, |input_handler| { - if !input_handler.apple_press_and_hold_enabled() { - input_handler.replace_text_in_range( - None, - &event.keystroke.ime_key.unwrap_or(event.keystroke.key), - ); + if let Some(key_char) = event.keystroke.key_char.as_ref() { + let handled = with_input_handler(&this, |input_handler| { + if !input_handler.apple_press_and_hold_enabled() { + input_handler.replace_text_in_range(None, &key_char); + return YES; + } + NO + }); + if handled == Some(YES) { return YES; } - NO - }); - if handled == Some(YES) { - return YES; } } @@ -1437,7 +1437,7 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) { let keystroke = Keystroke { modifiers: Default::default(), key: ".".into(), - ime_key: None, + key_char: None, }; let event = PlatformInput::KeyDown(KeyDownEvent { keystroke: keystroke.clone(), @@ -1755,15 +1755,21 @@ extern "C" fn attributed_substring_for_proposed_range( this: &Object, _: Sel, range: NSRange, - _actual_range: *mut c_void, + actual_range: *mut c_void, ) -> id { with_input_handler(this, |input_handler| { let range = range.to_range()?; if range.is_empty() { return None; } + let mut adjusted: Option> = None; - let selected_text = input_handler.text_for_range(range.clone())?; + let selected_text = input_handler.text_for_range(range.clone(), &mut adjusted)?; + if let Some(adjusted) = adjusted { + if adjusted != range { + unsafe { (actual_range as *mut NSRange).write(NSRange::from(adjusted)) }; + } + } unsafe { let string: id = msg_send![class!(NSAttributedString), alloc]; let string: id = msg_send![string, initWithString: ns_string(&selected_text)]; diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index d8ec6a718b..9c94aeaf2f 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -339,4 +339,9 @@ impl PlatformAtlas for TestAtlas { Ok(Some(state.tiles[key].clone())) } + + fn remove(&self, key: &AtlasKey) { + let mut state = self.0.lock(); + state.tiles.remove(key); + } } diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 1f501f3341..5f45d260d9 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -386,7 +386,7 @@ fn handle_char_msg( return Some(1); }; drop(lock); - let ime_key = keystroke.ime_key.clone(); + let key_char = keystroke.key_char.clone(); let event = KeyDownEvent { keystroke, is_held: lparam.0 & (0x1 << 30) > 0, @@ -397,7 +397,7 @@ fn handle_char_msg( if dispatch_event_result.default_prevented || !dispatch_event_result.propagate { return Some(0); } - let Some(ime_char) = ime_key else { + let Some(ime_char) = key_char else { return Some(1); }; with_input_handler(&state_ptr, |input_handler| { @@ -1160,6 +1160,8 @@ fn parse_syskeydown_msg_keystroke(wparam: WPARAM) -> Option { VK_END => "end", VK_PRIOR => "pageup", VK_NEXT => "pagedown", + VK_BROWSER_BACK => "back", + VK_BROWSER_FORWARD => "forward", VK_ESCAPE => "escape", VK_INSERT => "insert", VK_DELETE => "delete", @@ -1170,7 +1172,7 @@ fn parse_syskeydown_msg_keystroke(wparam: WPARAM) -> Option { Some(Keystroke { modifiers, key, - ime_key: None, + key_char: None, }) } @@ -1196,6 +1198,8 @@ fn parse_keydown_msg_keystroke(wparam: WPARAM) -> Option { VK_END => "end", VK_PRIOR => "pageup", VK_NEXT => "pagedown", + VK_BROWSER_BACK => "back", + VK_BROWSER_FORWARD => "forward", VK_ESCAPE => "escape", VK_INSERT => "insert", VK_DELETE => "delete", @@ -1216,7 +1220,7 @@ fn parse_keydown_msg_keystroke(wparam: WPARAM) -> Option { return Some(KeystrokeOrModifier::Keystroke(Keystroke { modifiers, key: format!("f{}", offset + 1), - ime_key: None, + key_char: None, })); }; return None; @@ -1227,7 +1231,7 @@ fn parse_keydown_msg_keystroke(wparam: WPARAM) -> Option { Some(KeystrokeOrModifier::Keystroke(Keystroke { modifiers, key, - ime_key: None, + key_char: None, })) } @@ -1249,7 +1253,7 @@ fn parse_char_msg_keystroke(wparam: WPARAM) -> Option { Some(Keystroke { modifiers, key, - ime_key: Some(first_char.to_string()), + key_char: Some(first_char.to_string()), }) } } @@ -1323,7 +1327,7 @@ fn basic_vkcode_to_string(code: u16, modifiers: Modifiers) -> Option Some(Keystroke { modifiers, key, - ime_key: None, + key_char: None, }) } diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 29443afabb..91e9816106 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -292,7 +292,7 @@ impl Platform for WindowsPlatform { pid, app_path.display(), ); - let restart_process = std::process::Command::new("powershell.exe") + let restart_process = util::command::new_std_command("powershell.exe") .arg("-command") .arg(script) .spawn(); diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index 46b54e689d..f2600d3c6f 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -647,11 +647,47 @@ impl PlatformWindow for WindowsWindow { } fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { - self.0 - .state - .borrow_mut() + let mut window_state = self.0.state.borrow_mut(); + window_state .renderer .update_transparency(background_appearance != WindowBackgroundAppearance::Opaque); + let mut version = unsafe { std::mem::zeroed() }; + let status = unsafe { windows::Wdk::System::SystemServices::RtlGetVersion(&mut version) }; + if status.is_ok() { + if background_appearance == WindowBackgroundAppearance::Blurred { + if version.dwBuildNumber >= 17763 { + set_window_composition_attribute(window_state.hwnd, Some((0, 0, 0, 10)), 4); + } + } else { + if version.dwBuildNumber >= 17763 { + set_window_composition_attribute(window_state.hwnd, None, 0); + } + } + //Transparent effect might cause some flickering and performance issues due `WS_EX_COMPOSITED` is enabled + //if `WS_EX_COMPOSITED` is removed the window instance won't initiate + if background_appearance == WindowBackgroundAppearance::Transparent { + unsafe { + let current_style = GetWindowLongW(window_state.hwnd, GWL_EXSTYLE); + SetWindowLongW( + window_state.hwnd, + GWL_EXSTYLE, + current_style | WS_EX_LAYERED.0 as i32 | WS_EX_COMPOSITED.0 as i32, + ); + SetLayeredWindowAttributes(window_state.hwnd, COLORREF(0), 225, LWA_ALPHA) + .inspect_err(|e| log::error!("Unable to set window to transparent: {e}")) + .ok(); + }; + } else { + unsafe { + let current_style = GetWindowLongW(window_state.hwnd, GWL_EXSTYLE); + SetWindowLongW( + window_state.hwnd, + GWL_EXSTYLE, + current_style & !WS_EX_LAYERED.0 as i32 & !WS_EX_COMPOSITED.0 as i32, + ); + } + } + } } fn minimize(&self) { @@ -932,6 +968,23 @@ struct StyleAndBounds { cy: i32, } +#[repr(C)] +struct WINDOWCOMPOSITIONATTRIBDATA { + attrib: u32, + pv_data: *mut std::ffi::c_void, + cb_data: usize, +} + +#[repr(C)] +struct AccentPolicy { + accent_state: u32, + accent_flags: u32, + gradient_color: u32, + animation_id: u32, +} + +type Color = (u8, u8, u8, u8); + #[derive(Debug, Default, Clone, Copy)] pub(crate) struct WindowBorderOffset { width_offset: i32, @@ -1136,6 +1189,44 @@ fn retrieve_window_placement( Ok(placement) } +fn set_window_composition_attribute(hwnd: HWND, color: Option, state: u32) { + unsafe { + type SetWindowCompositionAttributeType = + unsafe extern "system" fn(HWND, *mut WINDOWCOMPOSITIONATTRIBDATA) -> BOOL; + let module_name = PCSTR::from_raw("user32.dll\0".as_ptr()); + let user32 = GetModuleHandleA(module_name); + if user32.is_ok() { + let func_name = PCSTR::from_raw("SetWindowCompositionAttribute\0".as_ptr()); + let set_window_composition_attribute: SetWindowCompositionAttributeType = + std::mem::transmute(GetProcAddress(user32.unwrap(), func_name)); + let mut color = color.unwrap_or_default(); + let is_acrylic = state == 4; + if is_acrylic && color.3 == 0 { + color.3 = 1; + } + let accent = AccentPolicy { + accent_state: state, + accent_flags: if is_acrylic { 0 } else { 2 }, + gradient_color: (color.0 as u32) + | ((color.1 as u32) << 8) + | ((color.2 as u32) << 16) + | (color.3 as u32) << 24, + animation_id: 0, + }; + let mut data = WINDOWCOMPOSITIONATTRIBDATA { + attrib: 0x13, + pv_data: &accent as *const _ as *mut _, + cb_data: std::mem::size_of::(), + }; + let _ = set_window_composition_attribute(hwnd, &mut data as *mut _ as _); + } else { + let _ = user32 + .inspect_err(|e| log::error!("Error getting module: {e}")) + .ok(); + } + } +} + mod windows_renderer { use std::{num::NonZeroIsize, sync::Arc}; diff --git a/crates/gpui/src/prelude.rs b/crates/gpui/src/prelude.rs index 2ab115fa62..e1cc14e93e 100644 --- a/crates/gpui/src/prelude.rs +++ b/crates/gpui/src/prelude.rs @@ -5,5 +5,5 @@ pub use crate::{ util::FluentBuilder, BorrowAppContext, BorrowWindow, Context, Element, FocusableElement, InteractiveElement, IntoElement, ParentElement, Refineable, Render, RenderOnce, - StatefulInteractiveElement, Styled, VisualContext, + StatefulInteractiveElement, Styled, StyledImage, VisualContext, }; diff --git a/crates/gpui/src/svg_renderer.rs b/crates/gpui/src/svg_renderer.rs index a5cbb67372..f99880ec5e 100644 --- a/crates/gpui/src/svg_renderer.rs +++ b/crates/gpui/src/svg_renderer.rs @@ -10,7 +10,7 @@ pub(crate) struct RenderSvgParams { } #[derive(Clone)] -pub(crate) struct SvgRenderer { +pub struct SvgRenderer { asset_source: Arc, } @@ -24,7 +24,7 @@ impl SvgRenderer { Self { asset_source } } - pub fn render(&self, params: &RenderSvgParams) -> Result>> { + pub(crate) fn render(&self, params: &RenderSvgParams) -> Result>> { if params.size.is_zero() { return Err(anyhow!("can't render at a zero size")); } diff --git a/crates/gpui/src/text_system/line.rs b/crates/gpui/src/text_system/line.rs index b8b698a042..7c18684cbc 100644 --- a/crates/gpui/src/text_system/line.rs +++ b/crates/gpui/src/text_system/line.rs @@ -44,6 +44,21 @@ impl ShapedLine { self.layout.len } + /// Override the len, useful if you're rendering text a + /// as text b (e.g. rendering invisibles). + pub fn with_len(mut self, len: usize) -> Self { + let layout = self.layout.as_ref(); + self.layout = Arc::new(LineLayout { + font_size: layout.font_size, + width: layout.width, + ascent: layout.ascent, + descent: layout.descent, + runs: layout.runs.clone(), + len, + }); + self + } + /// Paint the line of text to the window. pub fn paint( &self, diff --git a/crates/gpui/src/text_system/line_layout.rs b/crates/gpui/src/text_system/line_layout.rs index 7e5a43dee8..66eb914a30 100644 --- a/crates/gpui/src/text_system/line_layout.rs +++ b/crates/gpui/src/text_system/line_layout.rs @@ -29,7 +29,7 @@ pub struct LineLayout { } /// A run of text that has been shaped . -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ShapedRun { /// The font id for this run pub font_id: FontId, diff --git a/crates/gpui/src/text_system/line_wrapper.rs b/crates/gpui/src/text_system/line_wrapper.rs index 3d38ca315c..1b99165eee 100644 --- a/crates/gpui/src/text_system/line_wrapper.rs +++ b/crates/gpui/src/text_system/line_wrapper.rs @@ -1,4 +1,4 @@ -use crate::{px, FontId, FontRun, Pixels, PlatformTextSystem, SharedString}; +use crate::{px, FontId, FontRun, Pixels, PlatformTextSystem, SharedString, TextRun}; use collections::HashMap; use std::{iter, sync::Arc}; @@ -104,6 +104,7 @@ impl LineWrapper { line: SharedString, truncate_width: Pixels, ellipsis: Option<&str>, + runs: &mut Vec, ) -> SharedString { let mut width = px(0.); let mut ellipsis_width = px(0.); @@ -124,15 +125,15 @@ impl LineWrapper { width += char_width; if width.floor() > truncate_width { - return SharedString::from(format!( - "{}{}", - &line[..truncate_ix], - ellipsis.unwrap_or("") - )); + let ellipsis = ellipsis.unwrap_or(""); + let result = SharedString::from(format!("{}{}", &line[..truncate_ix], ellipsis)); + update_runs_after_truncation(&result, ellipsis, runs); + + return result; } } - line.clone() + line } pub(crate) fn is_word_char(c: char) -> bool { @@ -195,6 +196,23 @@ impl LineWrapper { } } +fn update_runs_after_truncation(result: &str, ellipsis: &str, runs: &mut Vec) { + let mut truncate_at = result.len() - ellipsis.len(); + let mut run_end = None; + for (run_index, run) in runs.iter_mut().enumerate() { + if run.len <= truncate_at { + truncate_at -= run.len; + } else { + run.len = truncate_at + ellipsis.len(); + run_end = Some(run_index + 1); + break; + } + } + if let Some(run_end) = run_end { + runs.truncate(run_end); + } +} + /// A boundary between two lines of text. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct Boundary { @@ -213,7 +231,9 @@ impl Boundary { #[cfg(test)] mod tests { use super::*; - use crate::{font, TestAppContext, TestDispatcher}; + use crate::{ + font, Font, FontFeatures, FontStyle, FontWeight, Hsla, TestAppContext, TestDispatcher, + }; #[cfg(target_os = "macos")] use crate::{TextRun, WindowTextSystem, WrapBoundary}; use rand::prelude::*; @@ -232,6 +252,26 @@ mod tests { LineWrapper::new(id, px(16.), cx.text_system().platform_text_system.clone()) } + fn generate_test_runs(input_run_len: &[usize]) -> Vec { + input_run_len + .iter() + .map(|run_len| TextRun { + len: *run_len, + font: Font { + family: "Dummy".into(), + features: FontFeatures::default(), + fallbacks: None, + weight: FontWeight::default(), + style: FontStyle::Normal, + }, + color: Hsla::default(), + background_color: None, + underline: None, + strikethrough: None, + }) + .collect() + } + #[test] fn test_wrap_line() { let mut wrapper = build_wrapper(); @@ -293,28 +333,135 @@ mod tests { fn test_truncate_line() { let mut wrapper = build_wrapper(); - assert_eq!( - wrapper.truncate_line("aa bbb cccc ddddd eeee ffff gggg".into(), px(220.), None), - "aa bbb cccc ddddd eeee" + fn perform_test( + wrapper: &mut LineWrapper, + text: &'static str, + result: &'static str, + ellipsis: Option<&str>, + ) { + let dummy_run_lens = vec![text.len()]; + let mut dummy_runs = generate_test_runs(&dummy_run_lens); + assert_eq!( + wrapper.truncate_line(text.into(), px(220.), ellipsis, &mut dummy_runs), + result + ); + assert_eq!(dummy_runs.first().unwrap().len, result.len()); + } + + perform_test( + &mut wrapper, + "aa bbb cccc ddddd eeee ffff gggg", + "aa bbb cccc ddddd eeee", + None, ); - assert_eq!( - wrapper.truncate_line( - "aa bbb cccc ddddd eeee ffff gggg".into(), - px(220.), - Some("…") - ), - "aa bbb cccc ddddd eee…" + perform_test( + &mut wrapper, + "aa bbb cccc ddddd eeee ffff gggg", + "aa bbb cccc ddddd eee…", + Some("…"), ); - assert_eq!( - wrapper.truncate_line( - "aa bbb cccc ddddd eeee ffff gggg".into(), - px(220.), - Some("......") - ), - "aa bbb cccc dddd......" + perform_test( + &mut wrapper, + "aa bbb cccc ddddd eeee ffff gggg", + "aa bbb cccc dddd......", + Some("......"), ); } + #[test] + fn test_truncate_multiple_runs() { + let mut wrapper = build_wrapper(); + + fn perform_test( + wrapper: &mut LineWrapper, + text: &'static str, + result: &str, + run_lens: &[usize], + result_run_len: &[usize], + line_width: Pixels, + ) { + let mut dummy_runs = generate_test_runs(run_lens); + assert_eq!( + wrapper.truncate_line(text.into(), line_width, Some("…"), &mut dummy_runs), + result + ); + for (run, result_len) in dummy_runs.iter().zip(result_run_len) { + assert_eq!(run.len, *result_len); + } + } + // Case 0: Normal + // Text: abcdefghijkl + // Runs: Run0 { len: 12, ... } + // + // Truncate res: abcd… (truncate_at = 4) + // Run res: Run0 { string: abcd…, len: 7, ... } + perform_test(&mut wrapper, "abcdefghijkl", "abcd…", &[12], &[7], px(50.)); + // Case 1: Drop some runs + // Text: abcdefghijkl + // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } + // + // Truncate res: abcdef… (truncate_at = 6) + // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len: + // 5, ... } + perform_test( + &mut wrapper, + "abcdefghijkl", + "abcdef…", + &[4, 4, 4], + &[4, 5], + px(70.), + ); + // Case 2: Truncate at start of some run + // Text: abcdefghijkl + // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } + // + // Truncate res: abcdefgh… (truncate_at = 8) + // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len: + // 4, ... }, Run2 { string: …, len: 3, ... } + perform_test( + &mut wrapper, + "abcdefghijkl", + "abcdefgh…", + &[4, 4, 4], + &[4, 4, 3], + px(90.), + ); + } + + #[test] + fn test_update_run_after_truncation() { + fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) { + let mut dummy_runs = generate_test_runs(run_lens); + update_runs_after_truncation(result, "…", &mut dummy_runs); + for (run, result_len) in dummy_runs.iter().zip(result_run_lens) { + assert_eq!(run.len, *result_len); + } + } + // Case 0: Normal + // Text: abcdefghijkl + // Runs: Run0 { len: 12, ... } + // + // Truncate res: abcd… (truncate_at = 4) + // Run res: Run0 { string: abcd…, len: 7, ... } + perform_test("abcd…", &[12], &[7]); + // Case 1: Drop some runs + // Text: abcdefghijkl + // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } + // + // Truncate res: abcdef… (truncate_at = 6) + // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len: + // 5, ... } + perform_test("abcdef…", &[4, 4, 4], &[4, 5]); + // Case 2: Truncate at start of some run + // Text: abcdefghijkl + // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } + // + // Truncate res: abcdefgh… (truncate_at = 8) + // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len: + // 4, ... }, Run2 { string: …, len: 3, ... } + perform_test("abcdefgh…", &[4, 4, 4], &[4, 4, 3]); + } + #[test] fn test_is_word_char() { #[track_caller] diff --git a/crates/gpui/src/view.rs b/crates/gpui/src/view.rs index 7f10eb25c3..4f35413a27 100644 --- a/crates/gpui/src/view.rs +++ b/crates/gpui/src/view.rs @@ -7,6 +7,7 @@ use crate::{ }; use anyhow::{Context, Result}; use refineable::Refineable; +use std::mem; use std::{ any::{type_name, TypeId}, fmt, @@ -341,11 +342,13 @@ impl Element for AnyView { } } + let refreshing = mem::replace(&mut cx.window.refreshing, true); let prepaint_start = cx.prepaint_index(); let mut element = (self.render)(self, cx); element.layout_as_root(bounds.size.into(), cx); element.prepaint_at(bounds.origin, cx); let prepaint_end = cx.prepaint_index(); + cx.window.refreshing = refreshing; ( Some(element), @@ -382,7 +385,9 @@ impl Element for AnyView { let paint_start = cx.paint_index(); if let Some(element) = element { + let refreshing = mem::replace(&mut cx.window.refreshing, true); element.paint(cx); + cx.window.refreshing = refreshing; } else { cx.reuse_paint(element_state.paint_range.clone()); } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index ba974567a6..2b6f1d4a99 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -900,7 +900,13 @@ impl<'a> WindowContext<'a> { /// Indicate that this view has changed, which will invoke any observers and also mark the window as dirty. /// If this view or any of its ancestors are *cached*, notifying it will cause it or its ancestors to be redrawn. - pub fn notify(&mut self, view_id: EntityId) { + /// Note that this method will always cause a redraw, the entire window is refreshed if view_id is None. + pub fn notify(&mut self, view_id: Option) { + let Some(view_id) = view_id else { + self.refresh(); + return; + }; + for view_id in self .window .rendered_frame @@ -1165,13 +1171,7 @@ impl<'a> WindowContext<'a> { /// If called from within a view, it will notify that view on the next frame. Otherwise, it will refresh the entire window. pub fn request_animation_frame(&self) { let parent_id = self.parent_view_id(); - self.on_next_frame(move |cx| { - if let Some(parent_id) = parent_id { - cx.notify(parent_id) - } else { - cx.refresh() - } - }); + self.on_next_frame(move |cx| cx.notify(parent_id)); } /// Spawn the future returned by the given closure on the application thread pool. @@ -1982,9 +1982,7 @@ impl<'a> WindowContext<'a> { /// /// Note that the multiple calls to this method will only result in one `Asset::load` call at a /// time. - /// - /// This asset will not be cached by default, see [Self::use_cached_asset] - pub fn use_asset(&mut self, source: &A::Source) -> Option { + pub fn use_asset(&mut self, source: &A::Source) -> Option { let (task, is_first) = self.fetch_asset::(source); task.clone().now_or_never().or_else(|| { if is_first { @@ -1994,13 +1992,7 @@ impl<'a> WindowContext<'a> { |mut cx| async move { task.await; - cx.on_next_frame(move |cx| { - if let Some(parent_id) = parent_id { - cx.notify(parent_id) - } else { - cx.refresh() - } - }); + cx.on_next_frame(move |cx| cx.notify(parent_id)); } }) .detach(); @@ -2163,6 +2155,9 @@ impl<'a> WindowContext<'a> { /// A variant of `with_element_state` that allows the element's id to be optional. This is a convenience /// method for elements where the element id may or may not be assigned. Prefer using `with_element_state` /// when the element is guaranteed to have an id. + /// + /// The first option means 'no ID provided' + /// The second option means 'not yet initialized' pub fn with_optional_element_state( &mut self, global_id: Option<&GlobalElementId>, @@ -2690,6 +2685,20 @@ impl<'a> WindowContext<'a> { }); } + /// Removes an image from the sprite atlas. + pub fn drop_image(&mut self, data: Arc) -> Result<()> { + for frame_index in 0..data.frame_count() { + let params = RenderImageParams { + image_id: data.id, + frame_index, + }; + + self.window.sprite_atlas.remove(¶ms.clone().into()); + } + + Ok(()) + } + #[must_use] /// Add a node to the layout tree for the current frame. Takes the `Style` of the element for which /// layout is being requested, along with the layout ids of any children. This method is called during @@ -3043,7 +3052,7 @@ impl<'a> WindowContext<'a> { return true; } - if let Some(input) = keystroke.ime_key { + if let Some(input) = keystroke.key_char { if let Some(mut input_handler) = self.window.platform_window.take_input_handler() { input_handler.dispatch_input(&input, self); self.window.platform_window.set_input_handler(input_handler); @@ -3055,7 +3064,7 @@ impl<'a> WindowContext<'a> { } /// Represent this action as a key binding string, to display in the UI. - pub fn keystroke_text_for(&self, action: &dyn Action) -> String { + pub fn keystroke_text_for_action(&self, action: &dyn Action) -> String { self.bindings_for_action(action) .into_iter() .next() @@ -3070,6 +3079,26 @@ impl<'a> WindowContext<'a> { .unwrap_or_else(|| action.name().to_string()) } + /// Represent this action as a key binding string, to display in the UI. + pub fn keystroke_text_for_action_in( + &self, + action: &dyn Action, + focus_handle: &FocusHandle, + ) -> String { + self.bindings_for_action_in(action, focus_handle) + .into_iter() + .next() + .map(|binding| { + binding + .keystrokes() + .iter() + .map(ToString::to_string) + .collect::>() + .join(" ") + }) + .unwrap_or_else(|| action.name().to_string()) + } + /// Dispatch a mouse or keyboard event on the window. #[profiling::function] pub fn dispatch_event(&mut self, event: PlatformInput) -> DispatchEventResult { @@ -3252,7 +3281,7 @@ impl<'a> WindowContext<'a> { if let Some(key) = key { keystroke = Some(Keystroke { key: key.to_string(), - ime_key: None, + key_char: None, modifiers: Modifiers::default(), }); } @@ -3467,7 +3496,7 @@ impl<'a> WindowContext<'a> { if !self.propagate_event { continue 'replay; } - if let Some(input) = replay.keystroke.ime_key.as_ref().cloned() { + if let Some(input) = replay.keystroke.key_char.as_ref().cloned() { if let Some(mut input_handler) = self.window.platform_window.take_input_handler() { input_handler.dispatch_input(&input, self); self.window.platform_window.set_input_handler(input_handler) @@ -4227,7 +4256,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { /// Indicate that this view has changed, which will invoke any observers and also mark the window as dirty. /// If this view or any of its ancestors are *cached*, notifying it will cause it or its ancestors to be redrawn. pub fn notify(&mut self) { - self.window_cx.notify(self.view.entity_id()); + self.window_cx.notify(Some(self.view.entity_id())); } /// Register a callback to be invoked when the window is resized. diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 5e58cc49fb..1d03e77e76 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -116,7 +116,7 @@ impl Item for ImageView { .map(Icon::from_path) } - fn breadcrumb_location(&self) -> ToolbarItemLocation { + fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation { ToolbarItemLocation::PrimaryLeft } diff --git a/crates/indexed_docs/src/extension_indexed_docs_provider.rs b/crates/indexed_docs/src/extension_indexed_docs_provider.rs index ed006546fe..25b0f16357 100644 --- a/crates/indexed_docs/src/extension_indexed_docs_provider.rs +++ b/crates/indexed_docs/src/extension_indexed_docs_provider.rs @@ -3,9 +3,33 @@ use std::sync::Arc; use anyhow::Result; use async_trait::async_trait; -use extension::Extension; +use extension::{Extension, ExtensionHostProxy, ExtensionIndexedDocsProviderProxy}; +use gpui::AppContext; -use crate::{IndexedDocsDatabase, IndexedDocsProvider, PackageName, ProviderId}; +use crate::{ + IndexedDocsDatabase, IndexedDocsProvider, IndexedDocsRegistry, PackageName, ProviderId, +}; + +pub fn init(cx: &mut AppContext) { + let proxy = ExtensionHostProxy::default_global(cx); + proxy.register_indexed_docs_provider_proxy(IndexedDocsRegistryProxy { + indexed_docs_registry: IndexedDocsRegistry::global(cx), + }); +} + +struct IndexedDocsRegistryProxy { + indexed_docs_registry: Arc, +} + +impl ExtensionIndexedDocsProviderProxy for IndexedDocsRegistryProxy { + fn register_indexed_docs_provider(&self, extension: Arc, provider_id: Arc) { + self.indexed_docs_registry + .register_provider(Box::new(ExtensionIndexedDocsProvider::new( + extension, + ProviderId(provider_id), + ))); + } +} pub struct ExtensionIndexedDocsProvider { extension: Arc, diff --git a/crates/indexed_docs/src/indexed_docs.rs b/crates/indexed_docs/src/indexed_docs.rs index 95e5c62335..42672cd220 100644 --- a/crates/indexed_docs/src/indexed_docs.rs +++ b/crates/indexed_docs/src/indexed_docs.rs @@ -3,7 +3,14 @@ mod providers; mod registry; mod store; +use gpui::AppContext; + pub use crate::extension_indexed_docs_provider::ExtensionIndexedDocsProvider; pub use crate::providers::rustdoc::*; pub use crate::registry::*; pub use crate::store::*; + +pub fn init(cx: &mut AppContext) { + IndexedDocsRegistry::init_global(cx); + extension_indexed_docs_provider::init(cx); +} diff --git a/crates/indexed_docs/src/registry.rs b/crates/indexed_docs/src/registry.rs index fa3425466c..6332e6c4b0 100644 --- a/crates/indexed_docs/src/registry.rs +++ b/crates/indexed_docs/src/registry.rs @@ -20,7 +20,7 @@ impl IndexedDocsRegistry { GlobalIndexedDocsRegistry::global(cx).0.clone() } - pub fn init_global(cx: &mut AppContext) { + pub(crate) fn init_global(cx: &mut AppContext) { GlobalIndexedDocsRegistry::set_global( cx, GlobalIndexedDocsRegistry(Arc::new(Self::new(cx.background_executor().clone()))), diff --git a/crates/inline_completion/Cargo.toml b/crates/inline_completion/Cargo.toml new file mode 100644 index 0000000000..237b0ff43f --- /dev/null +++ b/crates/inline_completion/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "inline_completion" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/inline_completion.rs" + +[dependencies] +gpui.workspace = true +language.workspace = true +project.workspace = true +text.workspace = true diff --git a/crates/inline_completion/LICENSE-GPL b/crates/inline_completion/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/inline_completion/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/editor/src/inline_completion_provider.rs b/crates/inline_completion/src/inline_completion.rs similarity index 93% rename from crates/editor/src/inline_completion_provider.rs rename to crates/inline_completion/src/inline_completion.rs index 1085a6294e..689bc03174 100644 --- a/crates/editor/src/inline_completion_provider.rs +++ b/crates/inline_completion/src/inline_completion.rs @@ -1,9 +1,18 @@ -use crate::Direction; use gpui::{AppContext, Model, ModelContext}; use language::Buffer; use std::ops::Range; use text::{Anchor, Rope}; +// TODO: Find a better home for `Direction`. +// +// This should live in an ancestor crate of `editor` and `inline_completion`, +// but at time of writing there isn't an obvious spot. +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum Direction { + Prev, + Next, +} + pub enum InlayProposal { Hint(Anchor, project::InlayHint), Suggestion(Anchor, Rope), diff --git a/crates/inline_completion_button/Cargo.toml b/crates/inline_completion_button/Cargo.toml index 13b2bfa2ea..427d0dafd8 100644 --- a/crates/inline_completion_button/Cargo.toml +++ b/crates/inline_completion_button/Cargo.toml @@ -23,7 +23,6 @@ paths.workspace = true settings.workspace = true supermaven.workspace = true ui.workspace = true -util.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index 8f727fd2fe..5470678d38 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use copilot::{Copilot, CopilotCodeVerification, Status}; +use copilot::{Copilot, Status}; use editor::{scroll::Autoscroll, Editor}; use fs::Fs; use gpui::{ @@ -15,7 +15,6 @@ use language::{ use settings::{update_settings_file, Settings, SettingsStore}; use std::{path::Path, sync::Arc}; use supermaven::{AccountStatus, Supermaven}; -use util::ResultExt; use workspace::{ create_and_open_local_file, item::ItemHandle, @@ -29,8 +28,6 @@ use zed_actions::OpenBrowser; const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot"; -struct CopilotStartingToast; - struct CopilotErrorToast; pub struct InlineCompletionButton { @@ -221,7 +218,7 @@ impl InlineCompletionButton { pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext) -> View { let fs = self.fs.clone(); ContextMenu::build(cx, |menu, _| { - menu.entry("Sign In", None, initiate_sign_in) + menu.entry("Sign In", None, copilot::initiate_sign_in) .entry("Disable Copilot", None, { let fs = fs.clone(); move |cx| hide_copilot(fs.clone(), cx) @@ -484,68 +481,3 @@ fn hide_copilot(fs: Arc, cx: &mut AppContext) { .inline_completion_provider = Some(InlineCompletionProvider::None); }); } - -pub fn initiate_sign_in(cx: &mut WindowContext) { - let Some(copilot) = Copilot::global(cx) else { - return; - }; - let status = copilot.read(cx).status(); - let Some(workspace) = cx.window_handle().downcast::() else { - return; - }; - match status { - Status::Starting { task } => { - let Some(workspace) = cx.window_handle().downcast::() else { - return; - }; - - let Ok(workspace) = workspace.update(cx, |workspace, cx| { - workspace.show_toast( - Toast::new( - NotificationId::unique::(), - "Copilot is starting...", - ), - cx, - ); - workspace.weak_handle() - }) else { - return; - }; - - cx.spawn(|mut cx| async move { - task.await; - if let Some(copilot) = cx.update(|cx| Copilot::global(cx)).ok().flatten() { - workspace - .update(&mut cx, |workspace, cx| match copilot.read(cx).status() { - Status::Authorized => workspace.show_toast( - Toast::new( - NotificationId::unique::(), - "Copilot has started!", - ), - cx, - ), - _ => { - workspace.dismiss_toast( - &NotificationId::unique::(), - cx, - ); - copilot - .update(cx, |copilot, cx| copilot.sign_in(cx)) - .detach_and_log_err(cx); - } - }) - .log_err(); - } - }) - .detach(); - } - _ => { - copilot.update(cx, |this, cx| this.sign_in(cx)).detach(); - workspace - .update(cx, |this, cx| { - this.toggle_modal(cx, |cx| CopilotCodeVerification::new(&copilot, cx)); - }) - .ok(); - } - } -} diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 41285d8222..8b97d4a95f 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -31,6 +31,7 @@ async-watch.workspace = true clock.workspace = true collections.workspace = true ec4rs.workspace = true +fs.workspace = true futures.workspace = true fuzzy.workspace = true git.workspace = true diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 690b2d15ba..2479eafd7a 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -21,6 +21,7 @@ use async_watch as watch; use clock::Lamport; pub use clock::ReplicaId; use collections::HashMap; +use fs::MTime; use futures::channel::oneshot; use gpui::{ AnyElement, AppContext, Context as _, EventEmitter, HighlightStyle, Model, ModelContext, @@ -51,7 +52,7 @@ use std::{ path::{Path, PathBuf}, str, sync::{Arc, LazyLock}, - time::{Duration, Instant, SystemTime}, + time::{Duration, Instant}, vec, }; use sum_tree::TreeMap; @@ -104,10 +105,11 @@ pub struct Buffer { text: TextBuffer, diff_base: Option, git_diff: git::diff::BufferDiff, + /// Filesystem state, `None` when there is no path. file: Option>, /// The mtime of the file when this buffer was last loaded from /// or saved to disk. - saved_mtime: Option, + saved_mtime: Option, /// The version vector when this buffer was last loaded from /// or saved to disk. saved_version: clock::Global, @@ -371,8 +373,9 @@ pub trait File: Send + Sync { self.as_local().is_some() } - /// Returns the file's mtime. - fn mtime(&self) -> Option; + /// Returns whether the file is new, exists in storage, or has been deleted. Includes metadata + /// only available in some states, such as modification time. + fn disk_state(&self) -> DiskState; /// Returns the path of this file relative to the worktree's root directory. fn path(&self) -> &Arc; @@ -390,14 +393,6 @@ pub trait File: Send + Sync { /// This is needed for looking up project-specific settings. fn worktree_id(&self, cx: &AppContext) -> WorktreeId; - /// Returns whether the file has been deleted. - fn is_deleted(&self) -> bool; - - /// Returns whether the file existed on disk at one point - fn is_created(&self) -> bool { - self.mtime().is_some() - } - /// Converts this file into an [`Any`] trait object. fn as_any(&self) -> &dyn Any; @@ -408,6 +403,31 @@ pub trait File: Send + Sync { fn is_private(&self) -> bool; } +/// The file's storage status - whether it's stored (`Present`), and if so when it was last +/// modified. In the case where the file is not stored, it can be either `New` or `Deleted`. In the +/// UI these two states are distinguished. For example, the buffer tab does not display a deletion +/// indicator for new files. +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum DiskState { + /// File created in Zed that has not been saved. + New, + /// File present on the filesystem. + Present { mtime: MTime }, + /// Deleted file that was previously present. + Deleted, +} + +impl DiskState { + /// Returns the file's last known modification time on disk. + pub fn mtime(self) -> Option { + match self { + DiskState::New => None, + DiskState::Present { mtime } => Some(mtime), + DiskState::Deleted => None, + } + } +} + /// The file associated with a buffer, in the case where the file is on the local disk. pub trait LocalFile: File { /// Returns the absolute path of this file @@ -750,7 +770,7 @@ impl Buffer { file: Option>, capability: Capability, ) -> Self { - let saved_mtime = file.as_ref().and_then(|file| file.mtime()); + let saved_mtime = file.as_ref().and_then(|file| file.disk_state().mtime()); let snapshot = buffer.snapshot(); let git_diff = git::diff::BufferDiff::new(&snapshot); let syntax_map = Mutex::new(SyntaxMap::new(&snapshot)); @@ -954,7 +974,7 @@ impl Buffer { } /// The mtime of the buffer's file when the buffer was last saved or reloaded from disk. - pub fn saved_mtime(&self) -> Option { + pub fn saved_mtime(&self) -> Option { self.saved_mtime } @@ -989,7 +1009,7 @@ impl Buffer { pub fn did_save( &mut self, version: clock::Global, - mtime: Option, + mtime: Option, cx: &mut ModelContext, ) { self.saved_version = version; @@ -1014,7 +1034,7 @@ impl Buffer { self.reload_task = Some(cx.spawn(|this, mut cx| async move { let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| { let file = this.file.as_ref()?.as_local()?; - Some((file.mtime(), file.load(cx))) + Some((file.disk_state().mtime(), file.load(cx))) })? else { return Ok(()); @@ -1055,7 +1075,7 @@ impl Buffer { &mut self, version: clock::Global, line_ending: LineEnding, - mtime: Option, + mtime: Option, cx: &mut ModelContext, ) { self.saved_version = version; @@ -1070,6 +1090,7 @@ impl Buffer { /// Updates the [`File`] backing this buffer. This should be called when /// the file has changed or has been deleted. pub fn file_updated(&mut self, new_file: Arc, cx: &mut ModelContext) { + let was_dirty = self.is_dirty(); let mut file_changed = false; if let Some(old_file) = self.file.as_ref() { @@ -1077,21 +1098,12 @@ impl Buffer { file_changed = true; } - if new_file.is_deleted() { - if !old_file.is_deleted() { - file_changed = true; - if !self.is_dirty() { - cx.emit(BufferEvent::DirtyChanged); - } - } - } else { - let new_mtime = new_file.mtime(); - if new_mtime != old_file.mtime() { - file_changed = true; - - if !self.is_dirty() { - cx.emit(BufferEvent::ReloadNeeded); - } + let old_state = old_file.disk_state(); + let new_state = new_file.disk_state(); + if old_state != new_state { + file_changed = true; + if !was_dirty && matches!(new_state, DiskState::Present { .. }) { + cx.emit(BufferEvent::ReloadNeeded) } } } else { @@ -1101,6 +1113,9 @@ impl Buffer { self.file = Some(new_file); if file_changed { self.non_text_state_update_count += 1; + if was_dirty != self.is_dirty() { + cx.emit(BufferEvent::DirtyChanged); + } cx.emit(BufferEvent::FileHandleChanged); cx.notify(); } @@ -1742,20 +1757,31 @@ impl Buffer { pub fn is_dirty(&self) -> bool { self.capability != Capability::ReadOnly && (self.has_conflict - || self.has_unsaved_edits() - || self - .file - .as_ref() - .map_or(false, |file| file.is_deleted() || !file.is_created())) + || self.file.as_ref().map_or(false, |file| { + matches!(file.disk_state(), DiskState::New | DiskState::Deleted) + }) + || self.has_unsaved_edits()) } /// Checks if the buffer and its file have both changed since the buffer /// was last saved or reloaded. pub fn has_conflict(&self) -> bool { - self.has_conflict - || self.file.as_ref().map_or(false, |file| { - file.mtime() > self.saved_mtime && self.has_unsaved_edits() - }) + if self.has_conflict { + return true; + } + let Some(file) = self.file.as_ref() else { + return false; + }; + match file.disk_state() { + DiskState::New => false, + DiskState::Present { mtime } => match self.saved_mtime { + Some(saved_mtime) => { + mtime.bad_is_greater_than(saved_mtime) && self.has_unsaved_edits() + } + None => true, + }, + DiskState::Deleted => true, + } } /// Gets a [`Subscription`] that tracks all of the changes to the buffer's text. @@ -4396,7 +4422,7 @@ impl File for TestFile { None } - fn mtime(&self) -> Option { + fn disk_state(&self) -> DiskState { unimplemented!() } @@ -4408,10 +4434,6 @@ impl File for TestFile { WorktreeId::from_usize(0) } - fn is_deleted(&self) -> bool { - unimplemented!() - } - fn as_any(&self) -> &dyn std::any::Any { unimplemented!() } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 580955a98b..58be8a4dc3 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -201,13 +201,14 @@ impl CachedLspAdapter { pub async fn get_language_server_command( self: Arc, delegate: Arc, + toolchains: Arc, binary_options: LanguageServerBinaryOptions, cx: &mut AsyncAppContext, ) -> Result { let cached_binary = self.cached_binary.lock().await; self.adapter .clone() - .get_language_server_command(delegate, binary_options, cached_binary, cx) + .get_language_server_command(delegate, toolchains, binary_options, cached_binary, cx) .await } @@ -281,6 +282,7 @@ pub trait LspAdapter: 'static + Send + Sync { fn get_language_server_command<'a>( self: Arc, delegate: Arc, + toolchains: Arc, binary_options: LanguageServerBinaryOptions, mut cached_binary: futures::lock::MutexGuard<'a, Option>, cx: &'a mut AsyncAppContext, @@ -298,7 +300,7 @@ pub trait LspAdapter: 'static + Send + Sync { // because we don't want to download and overwrite our global one // for each worktree we might have open. if binary_options.allow_path_lookup { - if let Some(binary) = self.check_if_user_installed(delegate.as_ref(), cx).await { + if let Some(binary) = self.check_if_user_installed(delegate.as_ref(), toolchains, cx).await { log::info!( "found user-installed language server for {}. path: {:?}, arguments: {:?}", self.name().0, @@ -357,6 +359,7 @@ pub trait LspAdapter: 'static + Send + Sync { async fn check_if_user_installed( &self, _: &dyn LspAdapterDelegate, + _: Arc, _: &AsyncAppContext, ) -> Option { None @@ -1665,6 +1668,7 @@ impl LspAdapter for FakeLspAdapter { async fn check_if_user_installed( &self, _: &dyn LspAdapterDelegate, + _: Arc, _: &AsyncAppContext, ) -> Option { Some(self.language_server_binary.clone()) @@ -1673,6 +1677,7 @@ impl LspAdapter for FakeLspAdapter { fn get_language_server_command<'a>( self: Arc, _: Arc, + _: Arc, _: LanguageServerBinaryOptions, _: futures::lock::MutexGuard<'a, Option>, _: &'a mut AsyncAppContext, diff --git a/crates/language/src/markdown.rs b/crates/language/src/markdown.rs index b9393a16ab..0221f0f431 100644 --- a/crates/language/src/markdown.rs +++ b/crates/language/src/markdown.rs @@ -239,12 +239,7 @@ pub async fn parse_markdown_block( Event::Start(tag) => match tag { Tag::Paragraph => new_paragraph(text, &mut list_stack), - Tag::Heading { - level: _, - id: _, - classes: _, - attrs: _, - } => { + Tag::Heading { .. } => { new_paragraph(text, &mut list_stack); bold_depth += 1; } @@ -267,12 +262,7 @@ pub async fn parse_markdown_block( Tag::Strikethrough => strikethrough_depth += 1, - Tag::Link { - link_type: _, - dest_url, - title: _, - id: _, - } => link_url = Some(dest_url.to_string()), + Tag::Link { dest_url, .. } => link_url = Some(dest_url.to_string()), Tag::List(number) => { list_stack.push((number, false)); diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs index cd9a3bc403..fe8936db08 100644 --- a/crates/language/src/toolchain.rs +++ b/crates/language/src/toolchain.rs @@ -20,6 +20,8 @@ pub struct Toolchain { pub name: SharedString, pub path: SharedString, pub language_name: LanguageName, + /// Full toolchain data (including language-specific details) + pub as_json: serde_json::Value, } #[async_trait(?Send)] @@ -29,6 +31,8 @@ pub trait ToolchainLister: Send + Sync { worktree_root: PathBuf, project_env: Option>, ) -> ToolchainList; + // Returns a term which we should use in UI to refer to a toolchain. + fn term(&self) -> SharedString; } #[async_trait(?Send)] diff --git a/crates/language_extension/Cargo.toml b/crates/language_extension/Cargo.toml new file mode 100644 index 0000000000..3d1e4d0a64 --- /dev/null +++ b/crates/language_extension/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "language_extension" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/language_extension.rs" + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +collections.workspace = true +extension.workspace = true +futures.workspace = true +gpui.workspace = true +language.workspace = true +lsp.workspace = true +serde.workspace = true +serde_json.workspace = true +util.workspace = true diff --git a/crates/language_extension/LICENSE-GPL b/crates/language_extension/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/language_extension/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/extension_host/src/extension_lsp_adapter.rs b/crates/language_extension/src/extension_lsp_adapter.rs similarity index 91% rename from crates/extension_host/src/extension_lsp_adapter.rs rename to crates/language_extension/src/extension_lsp_adapter.rs index 8f83c68e31..3286e09e2d 100644 --- a/crates/extension_host/src/extension_lsp_adapter.rs +++ b/crates/language_extension/src/extension_lsp_adapter.rs @@ -1,22 +1,28 @@ +use std::any::Any; +use std::ops::Range; +use std::path::PathBuf; +use std::pin::Pin; +use std::sync::Arc; + use anyhow::{Context, Result}; use async_trait::async_trait; use collections::HashMap; -use extension::{Extension, WorktreeDelegate}; +use extension::{Extension, ExtensionLanguageServerProxy, WorktreeDelegate}; use futures::{Future, FutureExt}; use gpui::AsyncAppContext; use language::{ - CodeLabel, HighlightId, Language, LanguageName, LanguageToolchainStore, LspAdapter, - LspAdapterDelegate, + CodeLabel, HighlightId, Language, LanguageName, LanguageServerBinaryStatus, + LanguageToolchainStore, LspAdapter, LspAdapterDelegate, }; use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName}; use serde::Serialize; use serde_json::Value; -use std::ops::Range; -use std::{any::Any, path::PathBuf, pin::Pin, sync::Arc}; use util::{maybe, ResultExt}; +use crate::LanguageServerRegistryProxy; + /// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`]. -pub struct WorktreeDelegateAdapter(pub Arc); +struct WorktreeDelegateAdapter(pub Arc); #[async_trait] impl WorktreeDelegate for WorktreeDelegateAdapter { @@ -44,10 +50,60 @@ impl WorktreeDelegate for WorktreeDelegateAdapter { } } -pub struct ExtensionLspAdapter { - pub(crate) extension: Arc, - pub(crate) language_server_id: LanguageServerName, - pub(crate) language_name: LanguageName, +impl ExtensionLanguageServerProxy for LanguageServerRegistryProxy { + fn register_language_server( + &self, + extension: Arc, + language_server_id: LanguageServerName, + language: LanguageName, + ) { + self.language_registry.register_lsp_adapter( + language.clone(), + Arc::new(ExtensionLspAdapter::new( + extension, + language_server_id, + language, + )), + ); + } + + fn remove_language_server( + &self, + language: &LanguageName, + language_server_id: &LanguageServerName, + ) { + self.language_registry + .remove_lsp_adapter(language, language_server_id); + } + + fn update_language_server_status( + &self, + language_server_id: LanguageServerName, + status: LanguageServerBinaryStatus, + ) { + self.language_registry + .update_lsp_status(language_server_id, status); + } +} + +struct ExtensionLspAdapter { + extension: Arc, + language_server_id: LanguageServerName, + language_name: LanguageName, +} + +impl ExtensionLspAdapter { + fn new( + extension: Arc, + language_server_id: LanguageServerName, + language_name: LanguageName, + ) -> Self { + Self { + extension, + language_server_id, + language_name, + } + } } #[async_trait(?Send)] @@ -59,6 +115,7 @@ impl LspAdapter for ExtensionLspAdapter { fn get_language_server_command<'a>( self: Arc, delegate: Arc, + _: Arc, _: LanguageServerBinaryOptions, _: futures::lock::MutexGuard<'a, Option>, _: &'a mut AsyncAppContext, diff --git a/crates/language_extension/src/language_extension.rs b/crates/language_extension/src/language_extension.rs new file mode 100644 index 0000000000..d8ffc71d7c --- /dev/null +++ b/crates/language_extension/src/language_extension.rs @@ -0,0 +1,51 @@ +mod extension_lsp_adapter; + +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::Result; +use extension::{ExtensionGrammarProxy, ExtensionHostProxy, ExtensionLanguageProxy}; +use language::{LanguageMatcher, LanguageName, LanguageRegistry, LoadedLanguage}; + +pub fn init( + extension_host_proxy: Arc, + language_registry: Arc, +) { + let language_server_registry_proxy = LanguageServerRegistryProxy { language_registry }; + extension_host_proxy.register_grammar_proxy(language_server_registry_proxy.clone()); + extension_host_proxy.register_language_proxy(language_server_registry_proxy.clone()); + extension_host_proxy.register_language_server_proxy(language_server_registry_proxy); +} + +#[derive(Clone)] +struct LanguageServerRegistryProxy { + language_registry: Arc, +} + +impl ExtensionGrammarProxy for LanguageServerRegistryProxy { + fn register_grammars(&self, grammars: Vec<(Arc, PathBuf)>) { + self.language_registry.register_wasm_grammars(grammars) + } +} + +impl ExtensionLanguageProxy for LanguageServerRegistryProxy { + fn register_language( + &self, + language: LanguageName, + grammar: Option>, + matcher: LanguageMatcher, + load: Arc Result + Send + Sync + 'static>, + ) { + self.language_registry + .register_language(language, grammar, matcher, load); + } + + fn remove_languages( + &self, + languages_to_remove: &[LanguageName], + grammars_to_remove: &[Arc], + ) { + self.language_registry + .remove_languages(&languages_to_remove, &grammars_to_remove); + } +} diff --git a/crates/language_model/Cargo.toml b/crates/language_model/Cargo.toml index e88675bbae..0fc54d509d 100644 --- a/crates/language_model/Cargo.toml +++ b/crates/language_model/Cargo.toml @@ -13,57 +13,30 @@ path = "src/language_model.rs" doctest = false [features] -test-support = [ - "editor/test-support", - "language/test-support", - "project/test-support", - "text/test-support", -] +test-support = [] [dependencies] anthropic = { workspace = true, features = ["schemars"] } anyhow.workspace = true -client.workspace = true +base64.workspace = true collections.workspace = true -copilot = { workspace = true, features = ["schemars"] } -editor.workspace = true -feature_flags.workspace = true futures.workspace = true google_ai = { workspace = true, features = ["schemars"] } gpui.workspace = true http_client.workspace = true -inline_completion_button.workspace = true +image.workspace = true log.workspace = true -menu.workspace = true ollama = { workspace = true, features = ["schemars"] } open_ai = { workspace = true, features = ["schemars"] } parking_lot.workspace = true proto.workspace = true -project.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true -settings.workspace = true smol.workspace = true strum.workspace = true -telemetry_events.workspace = true -theme.workspace = true -thiserror.workspace = true -tiktoken-rs.workspace = true ui.workspace = true util.workspace = true -base64.workspace = true -image.workspace = true - [dev-dependencies] -ctor.workspace = true -editor = { workspace = true, features = ["test-support"] } -env_logger.workspace = true -language = { workspace = true, features = ["test-support"] } -log.workspace = true -project = { workspace = true, features = ["test-support"] } -proto = { workspace = true, features = ["test-support"] } -rand.workspace = true -text = { workspace = true, features = ["test-support"] } -unindent.workspace = true +gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/language_model/src/provider/fake.rs b/crates/language_model/src/fake_provider.rs similarity index 100% rename from crates/language_model/src/provider/fake.rs rename to crates/language_model/src/fake_provider.rs diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index a2f5a072a9..f9df34a2d1 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -1,23 +1,19 @@ -pub mod logging; mod model; -pub mod provider; mod rate_limiter; mod registry; mod request; mod role; -pub mod settings; + +#[cfg(any(test, feature = "test-support"))] +pub mod fake_provider; use anyhow::Result; -use client::{Client, UserStore}; use futures::FutureExt; use futures::{future::BoxFuture, stream::BoxStream, StreamExt, TryStreamExt as _}; -use gpui::{ - AnyElement, AnyView, AppContext, AsyncAppContext, Model, SharedString, Task, WindowContext, -}; +use gpui::{AnyElement, AnyView, AppContext, AsyncAppContext, SharedString, Task, WindowContext}; pub use model::*; -use project::Fs; use proto::Plan; -pub(crate) use rate_limiter::*; +pub use rate_limiter::*; pub use registry::*; pub use request::*; pub use role::*; @@ -27,14 +23,10 @@ use std::fmt; use std::{future::Future, sync::Arc}; use ui::IconName; -pub fn init( - user_store: Model, - client: Arc, - fs: Arc, - cx: &mut AppContext, -) { - settings::init(fs, cx); - registry::init(user_store, client, cx); +pub const ZED_CLOUD_PROVIDER_ID: &str = "zed.dev"; + +pub fn init(cx: &mut AppContext) { + registry::init(cx); } /// The availability of a [`LanguageModel`]. @@ -184,7 +176,7 @@ pub trait LanguageModel: Send + Sync { } #[cfg(any(test, feature = "test-support"))] - fn as_fake(&self) -> &provider::fake::FakeLanguageModel { + fn as_fake(&self) -> &fake_provider::FakeLanguageModel { unimplemented!() } } diff --git a/crates/language_model/src/registry.rs b/crates/language_model/src/registry.rs index 72dfd998d4..88b2e8301c 100644 --- a/crates/language_model/src/registry.rs +++ b/crates/language_model/src/registry.rs @@ -1,76 +1,17 @@ -use crate::provider::cloud::RefreshLlmTokenListener; use crate::{ - provider::{ - anthropic::AnthropicLanguageModelProvider, cloud::CloudLanguageModelProvider, - copilot_chat::CopilotChatLanguageModelProvider, google::GoogleLanguageModelProvider, - ollama::OllamaLanguageModelProvider, open_ai::OpenAiLanguageModelProvider, - }, LanguageModel, LanguageModelId, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderState, }; -use client::{Client, UserStore}; use collections::BTreeMap; use gpui::{AppContext, EventEmitter, Global, Model, ModelContext}; use std::sync::Arc; use ui::Context; -pub fn init(user_store: Model, client: Arc, cx: &mut AppContext) { - let registry = cx.new_model(|cx| { - let mut registry = LanguageModelRegistry::default(); - register_language_model_providers(&mut registry, user_store, client, cx); - registry - }); +pub fn init(cx: &mut AppContext) { + let registry = cx.new_model(|_cx| LanguageModelRegistry::default()); cx.set_global(GlobalLanguageModelRegistry(registry)); } -fn register_language_model_providers( - registry: &mut LanguageModelRegistry, - user_store: Model, - client: Arc, - cx: &mut ModelContext, -) { - use feature_flags::FeatureFlagAppExt; - - RefreshLlmTokenListener::register(client.clone(), cx); - - registry.register_provider( - AnthropicLanguageModelProvider::new(client.http_client(), cx), - cx, - ); - registry.register_provider( - OpenAiLanguageModelProvider::new(client.http_client(), cx), - cx, - ); - registry.register_provider( - OllamaLanguageModelProvider::new(client.http_client(), cx), - cx, - ); - registry.register_provider( - GoogleLanguageModelProvider::new(client.http_client(), cx), - cx, - ); - registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx); - - cx.observe_flag::(move |enabled, cx| { - let user_store = user_store.clone(); - let client = client.clone(); - LanguageModelRegistry::global(cx).update(cx, move |registry, cx| { - if enabled { - registry.register_provider( - CloudLanguageModelProvider::new(user_store.clone(), client.clone(), cx), - cx, - ); - } else { - registry.unregister_provider( - LanguageModelProviderId::from(crate::provider::cloud::PROVIDER_ID.to_string()), - cx, - ); - } - }); - }) - .detach(); -} - struct GlobalLanguageModelRegistry(Model); impl Global for GlobalLanguageModelRegistry {} @@ -106,8 +47,8 @@ impl LanguageModelRegistry { } #[cfg(any(test, feature = "test-support"))] - pub fn test(cx: &mut AppContext) -> crate::provider::fake::FakeLanguageModelProvider { - let fake_provider = crate::provider::fake::FakeLanguageModelProvider; + pub fn test(cx: &mut AppContext) -> crate::fake_provider::FakeLanguageModelProvider { + let fake_provider = crate::fake_provider::FakeLanguageModelProvider; let registry = cx.new_model(|cx| { let mut registry = Self::default(); registry.register_provider(fake_provider.clone(), cx); @@ -148,7 +89,7 @@ impl LanguageModelRegistry { } pub fn providers(&self) -> Vec> { - let zed_provider_id = LanguageModelProviderId(crate::provider::cloud::PROVIDER_ID.into()); + let zed_provider_id = LanguageModelProviderId("zed.dev".into()); let mut providers = Vec::with_capacity(self.providers.len()); if let Some(provider) = self.providers.get(&zed_provider_id) { providers.push(provider.clone()); @@ -269,7 +210,7 @@ impl LanguageModelRegistry { #[cfg(test)] mod tests { use super::*; - use crate::provider::fake::FakeLanguageModelProvider; + use crate::fake_provider::FakeLanguageModelProvider; #[gpui::test] fn test_register_providers(cx: &mut AppContext) { @@ -281,10 +222,10 @@ mod tests { let providers = registry.read(cx).providers(); assert_eq!(providers.len(), 1); - assert_eq!(providers[0].id(), crate::provider::fake::provider_id()); + assert_eq!(providers[0].id(), crate::fake_provider::provider_id()); registry.update(cx, |registry, cx| { - registry.unregister_provider(crate::provider::fake::provider_id(), cx); + registry.unregister_provider(crate::fake_provider::provider_id(), cx); }); let providers = registry.read(cx).providers(); diff --git a/crates/language_model_selector/Cargo.toml b/crates/language_model_selector/Cargo.toml new file mode 100644 index 0000000000..cd00af50c0 --- /dev/null +++ b/crates/language_model_selector/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "language_model_selector" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/language_model_selector.rs" + +[dependencies] +feature_flags.workspace = true +gpui.workspace = true +language_model.workspace = true +picker.workspace = true +proto.workspace = true +ui.workspace = true +workspace.workspace = true +zed_actions.workspace = true diff --git a/crates/language_model_selector/LICENSE-GPL b/crates/language_model_selector/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/language_model_selector/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/assistant/src/model_selector.rs b/crates/language_model_selector/src/language_model_selector.rs similarity index 90% rename from crates/assistant/src/model_selector.rs rename to crates/language_model_selector/src/language_model_selector.rs index 1b26b8b5ad..562bccbd88 100644 --- a/crates/assistant/src/model_selector.rs +++ b/crates/language_model_selector/src/language_model_selector.rs @@ -1,30 +1,27 @@ -use feature_flags::ZedPro; - -use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry}; -use proto::Plan; -use workspace::ShowConfiguration; - use std::sync::Arc; -use crate::assistant_settings::AssistantSettings; -use fs::Fs; -use gpui::{Action, AnyElement, DismissEvent, SharedString, Task}; +use feature_flags::ZedPro; +use gpui::{Action, AnyElement, AppContext, DismissEvent, SharedString, Task}; +use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry}; use picker::{Picker, PickerDelegate}; -use settings::update_settings_file; +use proto::Plan; use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, PopoverTrigger}; +use workspace::ShowConfiguration; const TRY_ZED_PRO_URL: &str = "https://zed.dev/pro"; +type OnModelChanged = Arc, &AppContext) + 'static>; + #[derive(IntoElement)] -pub struct ModelSelector { - handle: Option>>, - fs: Arc, +pub struct LanguageModelSelector { + handle: Option>>, + on_model_changed: OnModelChanged, trigger: T, info_text: Option, } -pub struct ModelPickerDelegate { - fs: Arc, +pub struct LanguageModelPickerDelegate { + on_model_changed: OnModelChanged, all_models: Vec, filtered_models: Vec, selected_index: usize, @@ -38,28 +35,34 @@ struct ModelInfo { is_selected: bool, } -impl ModelSelector { - pub fn new(fs: Arc, trigger: T) -> Self { - ModelSelector { +impl LanguageModelSelector { + pub fn new( + on_model_changed: impl Fn(Arc, &AppContext) + 'static, + trigger: T, + ) -> Self { + LanguageModelSelector { handle: None, - fs, + on_model_changed: Arc::new(on_model_changed), trigger, info_text: None, } } - pub fn with_handle(mut self, handle: PopoverMenuHandle>) -> Self { + pub fn with_handle( + mut self, + handle: PopoverMenuHandle>, + ) -> Self { self.handle = Some(handle); self } - pub fn with_info_text(mut self, text: impl Into) -> Self { + pub fn info_text(mut self, text: impl Into) -> Self { self.info_text = Some(text.into()); self } } -impl PickerDelegate for ModelPickerDelegate { +impl PickerDelegate for LanguageModelPickerDelegate { type ListItem = ListItem; fn match_count(&self) -> usize { @@ -137,9 +140,7 @@ impl PickerDelegate for ModelPickerDelegate { fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext>) { if let Some(model_info) = self.filtered_models.get(self.selected_index) { let model = model_info.model.clone(); - update_settings_file::(self.fs.clone(), cx, move |settings, _| { - settings.set_model(model.clone()) - }); + (self.on_model_changed)(model.clone(), cx); // Update the selection status let selected_model_id = model_info.model.id(); @@ -296,7 +297,7 @@ impl PickerDelegate for ModelPickerDelegate { } } -impl RenderOnce for ModelSelector { +impl RenderOnce for LanguageModelSelector { fn render(self, cx: &mut WindowContext) -> impl IntoElement { let selected_provider = LanguageModelRegistry::read_global(cx) .active_provider() @@ -331,8 +332,8 @@ impl RenderOnce for ModelSelector { }) .collect::>(); - let delegate = ModelPickerDelegate { - fs: self.fs.clone(), + let delegate = LanguageModelPickerDelegate { + on_model_changed: self.on_model_changed.clone(), all_models: all_models.clone(), filtered_models: all_models, selected_index: 0, diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml new file mode 100644 index 0000000000..00d948bd2d --- /dev/null +++ b/crates/language_models/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "language_models" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/language_models.rs" + +[dependencies] +anthropic = { workspace = true, features = ["schemars"] } +anyhow.workspace = true +client.workspace = true +collections.workspace = true +copilot = { workspace = true, features = ["schemars"] } +editor.workspace = true +feature_flags.workspace = true +fs.workspace = true +futures.workspace = true +google_ai = { workspace = true, features = ["schemars"] } +gpui.workspace = true +http_client.workspace = true +language_model.workspace = true +menu.workspace = true +ollama = { workspace = true, features = ["schemars"] } +open_ai = { workspace = true, features = ["schemars"] } +project.workspace = true +proto.workspace = true +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +settings.workspace = true +smol.workspace = true +strum.workspace = true +telemetry_events.workspace = true +theme.workspace = true +thiserror.workspace = true +tiktoken-rs.workspace = true +ui.workspace = true +util.workspace = true + +[dev-dependencies] +editor = { workspace = true, features = ["test-support"] } +language_model = { workspace = true, features = ["test-support"] } +project = { workspace = true, features = ["test-support"] } diff --git a/crates/language_models/LICENSE-GPL b/crates/language_models/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/language_models/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs new file mode 100644 index 0000000000..028ea0cfa4 --- /dev/null +++ b/crates/language_models/src/language_models.rs @@ -0,0 +1,80 @@ +use std::sync::Arc; + +use client::{Client, UserStore}; +use fs::Fs; +use gpui::{AppContext, Model, ModelContext}; +use language_model::{LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID}; + +mod logging; +pub mod provider; +mod settings; + +use crate::provider::anthropic::AnthropicLanguageModelProvider; +use crate::provider::cloud::{CloudLanguageModelProvider, RefreshLlmTokenListener}; +use crate::provider::copilot_chat::CopilotChatLanguageModelProvider; +use crate::provider::google::GoogleLanguageModelProvider; +use crate::provider::ollama::OllamaLanguageModelProvider; +use crate::provider::open_ai::OpenAiLanguageModelProvider; +pub use crate::settings::*; +pub use logging::report_assistant_event; + +pub fn init( + user_store: Model, + client: Arc, + fs: Arc, + cx: &mut AppContext, +) { + crate::settings::init(fs, cx); + let registry = LanguageModelRegistry::global(cx); + registry.update(cx, |registry, cx| { + register_language_model_providers(registry, user_store, client, cx); + }); +} + +fn register_language_model_providers( + registry: &mut LanguageModelRegistry, + user_store: Model, + client: Arc, + cx: &mut ModelContext, +) { + use feature_flags::FeatureFlagAppExt; + + RefreshLlmTokenListener::register(client.clone(), cx); + + registry.register_provider( + AnthropicLanguageModelProvider::new(client.http_client(), cx), + cx, + ); + registry.register_provider( + OpenAiLanguageModelProvider::new(client.http_client(), cx), + cx, + ); + registry.register_provider( + OllamaLanguageModelProvider::new(client.http_client(), cx), + cx, + ); + registry.register_provider( + GoogleLanguageModelProvider::new(client.http_client(), cx), + cx, + ); + registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx); + + cx.observe_flag::(move |enabled, cx| { + let user_store = user_store.clone(); + let client = client.clone(); + LanguageModelRegistry::global(cx).update(cx, move |registry, cx| { + if enabled { + registry.register_provider( + CloudLanguageModelProvider::new(user_store.clone(), client.clone(), cx), + cx, + ); + } else { + registry.unregister_provider( + LanguageModelProviderId::from(ZED_CLOUD_PROVIDER_ID.to_string()), + cx, + ); + } + }); + }) + .detach(); +} diff --git a/crates/language_model/src/logging.rs b/crates/language_models/src/logging.rs similarity index 100% rename from crates/language_model/src/logging.rs rename to crates/language_models/src/logging.rs diff --git a/crates/language_model/src/provider.rs b/crates/language_models/src/provider.rs similarity index 64% rename from crates/language_model/src/provider.rs rename to crates/language_models/src/provider.rs index d2d162b75e..fb79b12e4d 100644 --- a/crates/language_model/src/provider.rs +++ b/crates/language_models/src/provider.rs @@ -1,8 +1,6 @@ pub mod anthropic; pub mod cloud; pub mod copilot_chat; -#[cfg(any(test, feature = "test-support"))] -pub mod fake; pub mod google; pub mod ollama; pub mod open_ai; diff --git a/crates/language_model/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs similarity index 98% rename from crates/language_model/src/provider/anthropic.rs rename to crates/language_models/src/provider/anthropic.rs index 60e238b369..87460b824e 100644 --- a/crates/language_model/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -1,9 +1,4 @@ -use crate::{ - settings::AllLanguageModelSettings, LanguageModel, LanguageModelCacheConfiguration, - LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, - LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role, -}; -use crate::{LanguageModelCompletionEvent, LanguageModelToolUse, StopReason}; +use crate::AllLanguageModelSettings; use anthropic::{AnthropicError, ContentDelta, Event, ResponseContent}; use anyhow::{anyhow, Context as _, Result}; use collections::{BTreeMap, HashMap}; @@ -15,6 +10,12 @@ use gpui::{ View, WhiteSpace, }; use http_client::HttpClient; +use language_model::{ + LanguageModel, LanguageModelCacheConfiguration, LanguageModelId, LanguageModelName, + LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, + LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role, +}; +use language_model::{LanguageModelCompletionEvent, LanguageModelToolUse, StopReason}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; @@ -256,7 +257,7 @@ pub fn count_anthropic_tokens( let mut string_messages = Vec::with_capacity(messages.len()); for message in messages { - use crate::MessageContent; + use language_model::MessageContent; let mut string_contents = String::new(); diff --git a/crates/language_model/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs similarity index 98% rename from crates/language_model/src/provider/cloud.rs rename to crates/language_models/src/provider/cloud.rs index 41e23b56e3..f54e8c8d19 100644 --- a/crates/language_model/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -1,10 +1,4 @@ use super::open_ai::count_open_ai_tokens; -use crate::provider::anthropic::map_to_language_model_completion_events; -use crate::{ - settings::AllLanguageModelSettings, CloudModel, LanguageModel, LanguageModelCacheConfiguration, - LanguageModelId, LanguageModelName, LanguageModelProviderId, LanguageModelProviderName, - LanguageModelProviderState, LanguageModelRequest, RateLimiter, -}; use anthropic::AnthropicError; use anyhow::{anyhow, Result}; use client::{ @@ -22,6 +16,14 @@ use gpui::{ ModelContext, ReadGlobal, Subscription, Task, }; use http_client::{AsyncBody, HttpClient, Method, Response, StatusCode}; +use language_model::{ + CloudModel, LanguageModel, LanguageModelCacheConfiguration, LanguageModelId, LanguageModelName, + LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, + LanguageModelRequest, RateLimiter, ZED_CLOUD_PROVIDER_ID, +}; +use language_model::{ + LanguageModelAvailability, LanguageModelCompletionEvent, LanguageModelProvider, +}; use proto::TypedEnvelope; use schemars::JsonSchema; use serde::{de::DeserializeOwned, Deserialize, Serialize}; @@ -40,11 +42,11 @@ use strum::IntoEnumIterator; use thiserror::Error; use ui::{prelude::*, TintColor}; -use crate::{LanguageModelAvailability, LanguageModelCompletionEvent, LanguageModelProvider}; +use crate::provider::anthropic::map_to_language_model_completion_events; +use crate::AllLanguageModelSettings; use super::anthropic::count_anthropic_tokens; -pub const PROVIDER_ID: &str = "zed.dev"; pub const PROVIDER_NAME: &str = "Zed"; const ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: Option<&str> = @@ -255,7 +257,7 @@ impl LanguageModelProviderState for CloudLanguageModelProvider { impl LanguageModelProvider for CloudLanguageModelProvider { fn id(&self) -> LanguageModelProviderId { - LanguageModelProviderId(PROVIDER_ID.into()) + LanguageModelProviderId(ZED_CLOUD_PROVIDER_ID.into()) } fn name(&self) -> LanguageModelProviderName { @@ -535,7 +537,7 @@ impl LanguageModel for CloudLanguageModel { } fn provider_id(&self) -> LanguageModelProviderId { - LanguageModelProviderId(PROVIDER_ID.into()) + LanguageModelProviderId(ZED_CLOUD_PROVIDER_ID.into()) } fn provider_name(&self) -> LanguageModelProviderName { diff --git a/crates/language_model/src/provider/copilot_chat.rs b/crates/language_models/src/provider/copilot_chat.rs similarity index 97% rename from crates/language_model/src/provider/copilot_chat.rs rename to crates/language_models/src/provider/copilot_chat.rs index a991e81fbc..5ae1ad56c5 100644 --- a/crates/language_model/src/provider/copilot_chat.rs +++ b/crates/language_models/src/provider/copilot_chat.rs @@ -14,6 +14,11 @@ use gpui::{ percentage, svg, Animation, AnimationExt, AnyView, AppContext, AsyncAppContext, Model, Render, Subscription, Task, Transformation, }; +use language_model::{ + LanguageModel, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, + LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, + LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role, +}; use settings::SettingsStore; use std::time::Duration; use strum::IntoEnumIterator; @@ -23,12 +28,6 @@ use ui::{ ViewContext, VisualContext, WindowContext, }; -use crate::{ - LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider, - LanguageModelProviderId, LanguageModelProviderName, LanguageModelRequest, RateLimiter, Role, -}; -use crate::{LanguageModelCompletionEvent, LanguageModelProviderState}; - use super::anthropic::count_anthropic_tokens; use super::open_ai::count_open_ai_tokens; @@ -383,9 +382,7 @@ impl Render for ConfigurationView { .icon_size(IconSize::Medium) .style(ui::ButtonStyle::Filled) .full_width() - .on_click(|_, cx| { - inline_completion_button::initiate_sign_in(cx) - }), + .on_click(|_, cx| copilot::initiate_sign_in(cx)), ) .child( div().flex().w_full().items_center().child( diff --git a/crates/language_model/src/provider/google.rs b/crates/language_models/src/provider/google.rs similarity index 98% rename from crates/language_model/src/provider/google.rs rename to crates/language_models/src/provider/google.rs index 94d5ffca7d..59589605ee 100644 --- a/crates/language_model/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -8,6 +8,12 @@ use gpui::{ View, WhiteSpace, }; use http_client::HttpClient; +use language_model::LanguageModelCompletionEvent; +use language_model::{ + LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider, + LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, + LanguageModelRequest, RateLimiter, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; @@ -17,12 +23,7 @@ use theme::ThemeSettings; use ui::{prelude::*, Icon, IconName, Tooltip}; use util::ResultExt; -use crate::LanguageModelCompletionEvent; -use crate::{ - settings::AllLanguageModelSettings, LanguageModel, LanguageModelId, LanguageModelName, - LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, - LanguageModelProviderState, LanguageModelRequest, RateLimiter, -}; +use crate::AllLanguageModelSettings; const PROVIDER_ID: &str = "google"; const PROVIDER_NAME: &str = "Google AI"; diff --git a/crates/language_model/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs similarity index 97% rename from crates/language_model/src/provider/ollama.rs rename to crates/language_models/src/provider/ollama.rs index ac79bb2ed5..4fef43afe0 100644 --- a/crates/language_model/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -2,6 +2,12 @@ use anyhow::{anyhow, bail, Result}; use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt}; use gpui::{AnyView, AppContext, AsyncAppContext, ModelContext, Subscription, Task}; use http_client::HttpClient; +use language_model::LanguageModelCompletionEvent; +use language_model::{ + LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider, + LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, + LanguageModelRequest, RateLimiter, Role, +}; use ollama::{ get_models, preload_model, stream_chat_completion, ChatMessage, ChatOptions, ChatRequest, ChatResponseDelta, KeepAlive, OllamaToolCall, @@ -13,12 +19,7 @@ use std::{collections::BTreeMap, sync::Arc}; use ui::{prelude::*, ButtonLike, Indicator}; use util::ResultExt; -use crate::LanguageModelCompletionEvent; -use crate::{ - settings::AllLanguageModelSettings, LanguageModel, LanguageModelId, LanguageModelName, - LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, - LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role, -}; +use crate::AllLanguageModelSettings; const OLLAMA_DOWNLOAD_URL: &str = "https://ollama.com/download"; const OLLAMA_LIBRARY_URL: &str = "https://ollama.com/library"; @@ -35,7 +36,7 @@ pub struct OllamaSettings { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct AvailableModel { - /// The model name in the Ollama API (e.g. "llama3.1:latest") + /// The model name in the Ollama API (e.g. "llama3.2:latest") pub name: String, /// The model's name in Zed's UI, such as in the model selector dropdown menu in the assistant panel. pub display_name: Option, @@ -446,7 +447,7 @@ impl Render for ConfigurationView { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let is_authenticated = self.state.read(cx).is_authenticated(); - let ollama_intro = "Get up and running with Llama 3.1, Mistral, Gemma 2, and other large language models with Ollama."; + let ollama_intro = "Get up and running with Llama 3.2, Mistral, Gemma 2, and other large language models with Ollama."; let ollama_reqs = "Ollama must be running with at least one model installed to use it in the assistant."; @@ -475,7 +476,7 @@ impl Render for ConfigurationView { .bg(inline_code_bg) .px_1p5() .rounded_md() - .child(Label::new("ollama run llama3.1")), + .child(Label::new("ollama run llama3.2")), ), ), ) diff --git a/crates/language_model/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs similarity index 99% rename from crates/language_model/src/provider/open_ai.rs rename to crates/language_models/src/provider/open_ai.rs index 2a51b9a648..5c740f93e6 100644 --- a/crates/language_model/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -7,6 +7,11 @@ use gpui::{ View, WhiteSpace, }; use http_client::HttpClient; +use language_model::{ + LanguageModel, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, + LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, + LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role, +}; use open_ai::{ stream_completion, FunctionDefinition, ResponseStreamEvent, ToolChoice, ToolDefinition, }; @@ -19,12 +24,7 @@ use theme::ThemeSettings; use ui::{prelude::*, Icon, IconName, Tooltip}; use util::ResultExt; -use crate::LanguageModelCompletionEvent; -use crate::{ - settings::AllLanguageModelSettings, LanguageModel, LanguageModelId, LanguageModelName, - LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, - LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role, -}; +use crate::AllLanguageModelSettings; const PROVIDER_ID: &str = "openai"; const PROVIDER_NAME: &str = "OpenAI"; diff --git a/crates/language_model/src/settings.rs b/crates/language_models/src/settings.rs similarity index 97% rename from crates/language_model/src/settings.rs rename to crates/language_models/src/settings.rs index 275fcf0417..f6602427cb 100644 --- a/crates/language_model/src/settings.rs +++ b/crates/language_models/src/settings.rs @@ -2,22 +2,20 @@ use std::sync::Arc; use anyhow::Result; use gpui::AppContext; +use language_model::LanguageModelCacheConfiguration; use project::Fs; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{update_settings_file, Settings, SettingsSources}; -use crate::{ - provider::{ - self, - anthropic::AnthropicSettings, - cloud::{self, ZedDotDevSettings}, - copilot_chat::CopilotChatSettings, - google::GoogleSettings, - ollama::OllamaSettings, - open_ai::OpenAiSettings, - }, - LanguageModelCacheConfiguration, +use crate::provider::{ + self, + anthropic::AnthropicSettings, + cloud::{self, ZedDotDevSettings}, + copilot_chat::CopilotChatSettings, + google::GoogleSettings, + ollama::OllamaSettings, + open_ai::OpenAiSettings, }; /// Initializes the language model settings. diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index 96a44403bc..951423056e 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -29,7 +29,7 @@ load-grammars = [ "tree-sitter-rust", "tree-sitter-typescript", "tree-sitter-yaml", - "tree-sitter" + "tree-sitter", ] [dependencies] diff --git a/crates/languages/src/c.rs b/crates/languages/src/c.rs index a0e0f6dadb..8d0369f0e0 100644 --- a/crates/languages/src/c.rs +++ b/crates/languages/src/c.rs @@ -24,6 +24,7 @@ impl super::LspAdapter for CLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, + _: Arc, _: &AsyncAppContext, ) -> Option { let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; @@ -85,7 +86,7 @@ impl super::LspAdapter for CLspAdapter { } futures::io::copy(response.body_mut(), &mut file).await?; - let unzip_status = smol::process::Command::new("unzip") + let unzip_status = util::command::new_smol_command("unzip") .current_dir(&container_dir) .arg(&zip_path) .output() diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index 669f6918a9..6e2b5d464e 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -8,7 +8,7 @@ pub use language::*; use lsp::{LanguageServerBinary, LanguageServerName}; use regex::Regex; use serde_json::json; -use smol::{fs, process}; +use smol::fs; use std::{ any::Any, borrow::Cow, @@ -67,6 +67,7 @@ impl super::LspAdapter for GoLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, + _: Arc, _: &AsyncAppContext, ) -> Option { let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; @@ -138,8 +139,9 @@ impl super::LspAdapter for GoLspAdapter { let gobin_dir = container_dir.join("gobin"); fs::create_dir_all(&gobin_dir).await?; + let go = delegate.which("go".as_ref()).await.unwrap_or("go".into()); - let install_output = process::Command::new(go) + let install_output = util::command::new_smol_command(go) .env("GO111MODULE", "on") .env("GOBIN", &gobin_dir) .args(["install", "golang.org/x/tools/gopls@latest"]) @@ -157,7 +159,7 @@ impl super::LspAdapter for GoLspAdapter { } let installed_binary_path = gobin_dir.join("gopls"); - let version_output = process::Command::new(&installed_binary_path) + let version_output = util::command::new_smol_command(&installed_binary_path) .arg("version") .output() .await diff --git a/crates/languages/src/javascript/highlights.scm b/crates/languages/src/javascript/highlights.scm index a89375dee2..e5d4cb2068 100644 --- a/crates/languages/src/javascript/highlights.scm +++ b/crates/languages/src/javascript/highlights.scm @@ -150,6 +150,13 @@ "}" ] @punctuation.bracket +(ternary_expression + [ + "?" + ":" + ] @operator +) + [ "as" "async" diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 8ccd291b59..5cf8e76d32 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -9,7 +9,7 @@ use http_client::github::{latest_github_release, GitHubLspBinaryVersion}; use language::{LanguageRegistry, LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; use lsp::{LanguageServerBinary, LanguageServerName}; use node_runtime::NodeRuntime; -use project::ContextProviderWithTasks; +use project::{lsp_store::language_server_settings, ContextProviderWithTasks}; use serde_json::{json, Value}; use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore}; use smol::{ @@ -25,7 +25,7 @@ use std::{ sync::{Arc, OnceLock}, }; use task::{TaskTemplate, TaskTemplates, VariableName}; -use util::{fs::remove_matching, maybe, ResultExt}; +use util::{fs::remove_matching, maybe, merge_json_value_into, ResultExt}; const SERVER_PATH: &str = "node_modules/vscode-langservers-extracted/bin/vscode-json-language-server"; @@ -201,15 +201,26 @@ impl LspAdapter for JsonLspAdapter { async fn workspace_configuration( self: Arc, - _: &Arc, + delegate: &Arc, _: Arc, cx: &mut AsyncAppContext, ) -> Result { - cx.update(|cx| { + let mut config = cx.update(|cx| { self.workspace_config .get_or_init(|| Self::get_workspace_config(self.languages.language_names(), cx)) .clone() - }) + })?; + + let project_options = cx.update(|cx| { + language_server_settings(delegate.as_ref(), &self.name(), cx) + .and_then(|s| s.settings.clone()) + })?; + + if let Some(override_options) = project_options { + merge_json_value_into(override_options, &mut config); + } + + Ok(config) } fn language_ids(&self) -> HashMap { diff --git a/crates/languages/src/json/config.toml b/crates/languages/src/json/config.toml index c4a91c20b0..dc49f4f36e 100644 --- a/crates/languages/src/json/config.toml +++ b/crates/languages/src/json/config.toml @@ -1,6 +1,6 @@ name = "JSON" grammar = "json" -path_suffixes = ["json"] +path_suffixes = ["json", "flake.lock"] line_comments = ["// "] autoclose_before = ",]}" brackets = [ diff --git a/crates/languages/src/json/schemas/package.json b/crates/languages/src/json/schemas/package.json index 42c8f3c114..79d2457276 100644 --- a/crates/languages/src/json/schemas/package.json +++ b/crates/languages/src/json/schemas/package.json @@ -139,7 +139,7 @@ } }, "patternProperties": { - "^(?![\\.0-9]).": { + "^[^.0-9]+$": { "$ref": "#/definitions/packageExportsEntryOrFallback", "description": "The module path that is resolved when this environment matches the property name." } @@ -616,7 +616,7 @@ } } }, - "bundledDependencies": { + "bundleDependencies": { "description": "Array of package names that will be bundled when publishing the package.", "oneOf": [ { @@ -630,8 +630,8 @@ } ] }, - "bundleDependencies": { - "description": "DEPRECATED: This field is honored, but \"bundledDependencies\" is the correct field name.", + "bundledDependencies": { + "description": "DEPRECATED: This field is honored, but \"bundleDependencies\" is the correct field name.", "oneOf": [ { "type": "array", @@ -734,6 +734,9 @@ "registry": { "type": "string", "format": "uri" + }, + "provenance": { + "type": "boolean" } }, "additionalProperties": true diff --git a/crates/languages/src/json/schemas/tsconfig.json b/crates/languages/src/json/schemas/tsconfig.json index 808fc6f966..9174a58537 100644 --- a/crates/languages/src/json/schemas/tsconfig.json +++ b/crates/languages/src/json/schemas/tsconfig.json @@ -232,7 +232,7 @@ "$comment": "The value of 'null' is UNDOCUMENTED (https://github.com/microsoft/TypeScript/pull/18058).", "description": "Enable importing files with any extension, provided a declaration file is present.", "type": ["boolean", "null"], - "markdownDescription": "Enable importing files with any extension, provided a declaration file is present.\n\nSee more: https://www.typescriptlang.org/tsconfig#allowImportingTsExtensions" + "markdownDescription": "Enable importing files with any extension, provided a declaration file is present.\n\nSee more: https://www.typescriptlang.org/tsconfig#allowArbitraryExtensions" }, "allowImportingTsExtensions": { "$comment": "The value of 'null' is UNDOCUMENTED (https://github.com/microsoft/TypeScript/pull/18058).", @@ -426,17 +426,17 @@ "anyOf": [ { "enum": [ - "Classic", - "Node", - "Node10", - "Node16", - "NodeNext", - "Bundler" + "classic", + "node", + "node10", + "node16", + "nodenext", + "bundler" ], "markdownEnumDescriptions": [ - "It’s recommended to use `\"Node16\"` instead", - "Deprecated, use `\"Node10\"` in TypeScript 5.0+ instead", - "It’s recommended to use `\"Node16\"` instead", + "It’s recommended to use `\"node16\"` instead", + "Deprecated, use `\"node10\"` in TypeScript 5.0+ instead", + "It’s recommended to use `\"node16\"` instead", "This is the recommended setting for libraries and Node.js applications", "This is the recommended setting for libraries and Node.js applications", "This is the recommended setting in TypeScript 5.0+ for applications that use a bundler" @@ -497,10 +497,10 @@ }, "noUnusedLocals": { "$comment": "The value of 'null' is UNDOCUMENTED (https://github.com/microsoft/TypeScript/pull/18058).", - "description": "Enable error reporting when a local variables aren't read.", + "description": "Enable error reporting when a local variable isn't read.", "type": ["boolean", "null"], "default": false, - "markdownDescription": "Enable error reporting when a local variables aren't read.\n\nSee more: https://www.typescriptlang.org/tsconfig#noUnusedLocals" + "markdownDescription": "Enable error reporting when a local variable isn't read.\n\nSee more: https://www.typescriptlang.org/tsconfig#noUnusedLocals" }, "noUnusedParameters": { "$comment": "The value of 'null' is UNDOCUMENTED (https://github.com/microsoft/TypeScript/pull/18058).", @@ -949,14 +949,19 @@ "ESNext.Array", "ESNext.AsyncIterable", "ESNext.BigInt", + "ESNext.Collection", "ESNext.Intl", + "ESNext.Object", "ESNext.Promise", + "ESNext.Regexp", "ESNext.String", "ESNext.Symbol", "DOM", + "DOM.AsyncIterable", "DOM.Iterable", "ScriptHost", "WebWorker", + "WebWorker.AsyncIterable", "WebWorker.ImportScripts", "Webworker.Iterable", "ES7", @@ -1022,13 +1027,13 @@ "pattern": "^[Ee][Ss][Nn][Ee][Xx][Tt](\\.([Aa][Rr][Rr][Aa][Yy]|[Aa][Ss][Yy][Nn][Cc][Ii][Tt][Ee][Rr][Aa][Bb][Ll][Ee]|[Bb][Ii][Gg][Ii][Nn][Tt]|[Ii][Nn][Tt][Ll]|[Pp][Rr][Oo][Mm][Ii][Ss][Ee]|[Ss][Tt][Rr][Ii][Nn][Gg]|[Ss][Yy][Mm][Bb][Oo][Ll]|[Ww][Ee][Aa][Kk][Rr][Ee][Ff]|[Dd][Ee][Cc][Oo][Rr][Aa][Tt][Oo][Rr][Ss]|[Dd][Ii][Ss][Pp][Oo][Ss][Aa][Bb][Ll][Ee]))?$" }, { - "pattern": "^[Dd][Oo][Mm](\\.[Ii][Tt][Ee][Rr][Aa][Bb][Ll][Ee])?$" + "pattern": "^[Dd][Oo][Mm](\\.([Aa][Ss][Yy][Nn][Cc])?[Ii][Tt][Ee][Rr][Aa][Bb][Ll][Ee])?$" }, { "pattern": "^[Ss][Cc][Rr][Ii][Pp][Tt][Hh][Oo][Ss][Tt]$" }, { - "pattern": "^[Ww][Ee][Bb][Ww][Oo][Rr][Kk][Ee][Rr](\\.([Ii][Mm][Pp][Oo][Rr][Tt][Ss][Cc][Rr][Ii][Pp][Tt][Ss]|[Ii][Tt][Ee][Rr][Aa][Bb][Ll][Ee]))?$" + "pattern": "^[Ww][Ee][Bb][Ww][Oo][Rr][Kk][Ee][Rr](\\.([Ii][Mm][Pp][Oo][Rr][Tt][Ss][Cc][Rr][Ii][Pp][Tt][Ss]|([Aa][Ss][Yy][Nn][Cc])?[Ii][Tt][Ee][Rr][Aa][Bb][Ll][Ee]))?$" }, { "pattern": "^[Dd][Ee][Cc][Oo][Rr][Aa][Tt][Oo][Rr][Ss](\\.([Ll][Ee][Gg][Aa][Cc][Yy]))?$" @@ -1203,6 +1208,34 @@ "description": "Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting.", "type": ["boolean", "null"], "markdownDescription": "Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting.\n\nSee more: https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax" + }, + "noCheck": { + "$comment": "The value of 'null' is UNDOCUMENTED (https://github.com/microsoft/TypeScript/pull/18058).", + "description": "Disable full type checking (only critical parse and emit errors will be reported)", + "type": ["boolean", "null"], + "default": false, + "markdownDescription": "Disable full type checking (only critical parse and emit errors will be reported)\n\nSee more: https://www.typescriptlang.org/tsconfig#noCheck" + }, + "isolatedDeclarations": { + "$comment": "The value of 'null' is UNDOCUMENTED (https://github.com/microsoft/TypeScript/pull/18058).", + "description": "Require sufficient annotation on exports so other tools can trivially generate declaration files.", + "type": ["boolean", "null"], + "default": false, + "markdownDescription": "Require sufficient annotation on exports so other tools can trivially generate declaration files.\n\nSee more: https://www.typescriptlang.org/tsconfig#isolatedDeclarations" + }, + "noUncheckedSideEffectImports": { + "$comment": "The value of 'null' is UNDOCUMENTED (https://github.com/microsoft/TypeScript/pull/18058).", + "description": "Check side effect imports.", + "type": ["boolean", "null"], + "default": false, + "markdownDescription": "Check side effect imports.\n\nSee more: https://www.typescriptlang.org/tsconfig#noUncheckedSideEffectImports" + }, + "strictBuiltinIteratorReturn": { + "$comment": "The value of 'null' is UNDOCUMENTED (https://github.com/microsoft/TypeScript/pull/18058).", + "description": "Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'.", + "type": ["boolean", "null"], + "default": false, + "markdownDescription": "Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'.\n\nSee more: https://www.typescriptlang.org/tsconfig#strictBuiltinIteratorReturn" } } } @@ -1423,4 +1456,3 @@ "title": "JSON schema for the TypeScript compiler's configuration file", "type": "object" } - diff --git a/crates/languages/src/jsonc/config.toml b/crates/languages/src/jsonc/config.toml index fe62764b27..226ae92912 100644 --- a/crates/languages/src/jsonc/config.toml +++ b/crates/languages/src/jsonc/config.toml @@ -1,6 +1,6 @@ name = "JSONC" grammar = "jsonc" -path_suffixes = ["jsonc"] +path_suffixes = ["jsonc", "tsconfig.json", "pyrightconfig.json"] line_comments = ["// "] autoclose_before = ",]}" brackets = [ diff --git a/crates/languages/src/markdown/injections.scm b/crates/languages/src/markdown/injections.scm index 4b2493d4ce..5972a43eb1 100644 --- a/crates/languages/src/markdown/injections.scm +++ b/crates/languages/src/markdown/injections.scm @@ -5,3 +5,6 @@ ((inline) @content (#set! "language" "markdown-inline")) + +((html_block) @content + (#set! "language" "html")) diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 1e855777b2..8736a12942 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -2,8 +2,9 @@ use anyhow::ensure; use anyhow::{anyhow, Result}; use async_trait::async_trait; use collections::HashMap; -use gpui::AsyncAppContext; use gpui::{AppContext, Task}; +use gpui::{AsyncAppContext, SharedString}; +use language::language_settings::language_settings; use language::LanguageName; use language::LanguageToolchainStore; use language::Toolchain; @@ -18,9 +19,10 @@ use pet_core::python_environment::PythonEnvironmentKind; use pet_core::Configuration; use project::lsp_store::language_server_settings; use serde_json::{json, Value}; -use smol::{lock::OnceCell, process::Command}; +use smol::lock::OnceCell; use std::cmp::Ordering; +use std::str::FromStr; use std::sync::Mutex; use std::{ any::Any, @@ -35,6 +37,23 @@ use util::ResultExt; const SERVER_PATH: &str = "node_modules/pyright/langserver.index.js"; const NODE_MODULE_RELATIVE_SERVER_PATH: &str = "pyright/langserver.index.js"; +enum TestRunner { + UNITTEST, + PYTEST, +} + +impl FromStr for TestRunner { + type Err = (); + + fn from_str(s: &str) -> std::result::Result { + match s { + "unittest" => Ok(Self::UNITTEST), + "pytest" => Ok(Self::PYTEST), + _ => Err(()), + } + } +} + fn server_binary_arguments(server_path: &Path) -> Vec { vec![server_path.into(), "--stdio".into()] } @@ -60,6 +79,7 @@ impl LspAdapter for PythonLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, + _: Arc, _: &AsyncAppContext, ) -> Option { let node = delegate.which("node".as_ref()).await?; @@ -265,8 +285,8 @@ async fn get_cached_server_binary( pub(crate) struct PythonContextProvider; -const PYTHON_UNITTEST_TARGET_TASK_VARIABLE: VariableName = - VariableName::Custom(Cow::Borrowed("PYTHON_UNITTEST_TARGET")); +const PYTHON_TEST_TARGET_TASK_VARIABLE: VariableName = + VariableName::Custom(Cow::Borrowed("PYTHON_TEST_TARGET")); const PYTHON_ACTIVE_TOOLCHAIN_PATH: VariableName = VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN")); @@ -279,28 +299,16 @@ impl ContextProvider for PythonContextProvider { toolchains: Arc, cx: &mut gpui::AppContext, ) -> Task> { - let python_module_name = python_module_name_from_relative_path( - variables.get(&VariableName::RelativeFile).unwrap_or(""), - ); - let unittest_class_name = - variables.get(&VariableName::Custom(Cow::Borrowed("_unittest_class_name"))); - let unittest_method_name = variables.get(&VariableName::Custom(Cow::Borrowed( - "_unittest_method_name", - ))); + let test_target = { + let test_runner = selected_test_runner(location.buffer.read(cx).file(), cx); - let unittest_target_str = match (unittest_class_name, unittest_method_name) { - (Some(class_name), Some(method_name)) => { - format!("{}.{}.{}", python_module_name, class_name, method_name) - } - (Some(class_name), None) => format!("{}.{}", python_module_name, class_name), - (None, None) => python_module_name, - (None, Some(_)) => return Task::ready(Ok(task::TaskVariables::default())), // should never happen, a TestCase class is the unit of testing + let runner = match test_runner { + TestRunner::UNITTEST => self.build_unittest_target(variables), + TestRunner::PYTEST => self.build_pytest_target(variables), + }; + runner }; - let unittest_target = ( - PYTHON_UNITTEST_TARGET_TASK_VARIABLE.clone(), - unittest_target_str, - ); let worktree_id = location.buffer.read(cx).file().map(|f| f.worktree_id(cx)); cx.spawn(move |mut cx| async move { let active_toolchain = if let Some(worktree_id) = worktree_id { @@ -312,53 +320,174 @@ impl ContextProvider for PythonContextProvider { String::from("python3") }; let toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH, active_toolchain); - Ok(task::TaskVariables::from_iter([unittest_target, toolchain])) + Ok(task::TaskVariables::from_iter([test_target?, toolchain])) }) } fn associated_tasks( &self, - _: Option>, - _: &AppContext, + file: Option>, + cx: &AppContext, ) -> Option { - Some(TaskTemplates(vec![ + let test_runner = selected_test_runner(file.as_ref(), cx); + + let mut tasks = vec![ + // Execute a selection TaskTemplate { label: "execute selection".to_owned(), command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), args: vec!["-c".to_owned(), VariableName::SelectedText.template_value()], ..TaskTemplate::default() }, + // Execute an entire file TaskTemplate { label: format!("run '{}'", VariableName::File.template_value()), command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), args: vec![VariableName::File.template_value()], ..TaskTemplate::default() }, - TaskTemplate { - label: format!("unittest '{}'", VariableName::File.template_value()), - command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), - args: vec![ - "-m".to_owned(), - "unittest".to_owned(), - VariableName::File.template_value(), - ], - ..TaskTemplate::default() - }, - TaskTemplate { - label: "unittest $ZED_CUSTOM_PYTHON_UNITTEST_TARGET".to_owned(), - command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), - args: vec![ - "-m".to_owned(), - "unittest".to_owned(), - "$ZED_CUSTOM_PYTHON_UNITTEST_TARGET".to_owned(), - ], - tags: vec![ - "python-unittest-class".to_owned(), - "python-unittest-method".to_owned(), - ], - ..TaskTemplate::default() - }, - ])) + ]; + + tasks.extend(match test_runner { + TestRunner::UNITTEST => { + [ + // Run tests for an entire file + TaskTemplate { + label: format!("unittest '{}'", VariableName::File.template_value()), + command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), + args: vec![ + "-m".to_owned(), + "unittest".to_owned(), + VariableName::File.template_value(), + ], + ..TaskTemplate::default() + }, + // Run test(s) for a specific target within a file + TaskTemplate { + label: "unittest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(), + command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), + args: vec![ + "-m".to_owned(), + "unittest".to_owned(), + "$ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(), + ], + tags: vec![ + "python-unittest-class".to_owned(), + "python-unittest-method".to_owned(), + ], + ..TaskTemplate::default() + }, + ] + } + TestRunner::PYTEST => { + [ + // Run tests for an entire file + TaskTemplate { + label: format!("pytest '{}'", VariableName::File.template_value()), + command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), + args: vec![ + "-m".to_owned(), + "pytest".to_owned(), + VariableName::File.template_value(), + ], + ..TaskTemplate::default() + }, + // Run test(s) for a specific target within a file + TaskTemplate { + label: "pytest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(), + command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), + args: vec![ + "-m".to_owned(), + "pytest".to_owned(), + "$ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(), + ], + tags: vec![ + "python-pytest-class".to_owned(), + "python-pytest-method".to_owned(), + ], + ..TaskTemplate::default() + }, + ] + } + }); + + Some(TaskTemplates(tasks)) + } +} + +fn selected_test_runner(location: Option<&Arc>, cx: &AppContext) -> TestRunner { + const TEST_RUNNER_VARIABLE: &str = "TEST_RUNNER"; + language_settings(Some(LanguageName::new("Python")), location, cx) + .tasks + .variables + .get(TEST_RUNNER_VARIABLE) + .and_then(|val| TestRunner::from_str(val).ok()) + .unwrap_or(TestRunner::PYTEST) +} + +impl PythonContextProvider { + fn build_unittest_target( + &self, + variables: &task::TaskVariables, + ) -> Result<(VariableName, String)> { + let python_module_name = python_module_name_from_relative_path( + variables.get(&VariableName::RelativeFile).unwrap_or(""), + ); + + let unittest_class_name = + variables.get(&VariableName::Custom(Cow::Borrowed("_unittest_class_name"))); + + let unittest_method_name = variables.get(&VariableName::Custom(Cow::Borrowed( + "_unittest_method_name", + ))); + + let unittest_target_str = match (unittest_class_name, unittest_method_name) { + (Some(class_name), Some(method_name)) => { + format!("{}.{}.{}", python_module_name, class_name, method_name) + } + (Some(class_name), None) => format!("{}.{}", python_module_name, class_name), + (None, None) => python_module_name, + (None, Some(_)) => return Ok((VariableName::Custom(Cow::Borrowed("")), String::new())), // should never happen, a TestCase class is the unit of testing + }; + + let unittest_target = ( + PYTHON_TEST_TARGET_TASK_VARIABLE.clone(), + unittest_target_str, + ); + + Ok(unittest_target) + } + + fn build_pytest_target( + &self, + variables: &task::TaskVariables, + ) -> Result<(VariableName, String)> { + let file_path = variables + .get(&VariableName::RelativeFile) + .ok_or_else(|| anyhow!("No file path given"))?; + + let pytest_class_name = + variables.get(&VariableName::Custom(Cow::Borrowed("_pytest_class_name"))); + + let pytest_method_name = + variables.get(&VariableName::Custom(Cow::Borrowed("_pytest_method_name"))); + + let pytest_target_str = match (pytest_class_name, pytest_method_name) { + (Some(class_name), Some(method_name)) => { + format!("{}::{}::{}", file_path, class_name, method_name) + } + (Some(class_name), None) => { + format!("{}::{}", file_path, class_name) + } + (None, Some(method_name)) => { + format!("{}::{}", file_path, method_name) + } + (None, None) => file_path.to_string(), + }; + + let pytest_target = (PYTHON_TEST_TARGET_TASK_VARIABLE.clone(), pytest_target_str); + + Ok(pytest_target) } } @@ -370,8 +499,17 @@ fn python_module_name_from_relative_path(relative_path: &str) -> String { .to_string() } -#[derive(Default)] -pub(crate) struct PythonToolchainProvider {} +pub(crate) struct PythonToolchainProvider { + term: SharedString, +} + +impl Default for PythonToolchainProvider { + fn default() -> Self { + Self { + term: SharedString::new_static("Virtual Environment"), + } + } +} static ENV_PRIORITY_LIST: &'static [PythonEnvironmentKind] = &[ // Prioritize non-Conda environments. @@ -463,8 +601,9 @@ impl ToolchainLister for PythonToolchainProvider { .into(); Some(Toolchain { name, - path: toolchain.executable?.to_str()?.to_owned().into(), + path: toolchain.executable.as_ref()?.to_str()?.to_owned().into(), language_name: LanguageName::new("Python"), + as_json: serde_json::to_value(toolchain).ok()?, }) }) .collect(); @@ -475,6 +614,9 @@ impl ToolchainLister for PythonToolchainProvider { groups: Default::default(), } } + fn term(&self) -> SharedString { + self.term.clone() + } } pub struct EnvironmentApi<'a> { @@ -570,7 +712,7 @@ impl PyLspAdapter { let mut path = PathBuf::from(work_dir.as_ref()); path.push("pylsp-venv"); if !path.exists() { - Command::new(python_path) + util::command::new_smol_command(python_path) .arg("-m") .arg("venv") .arg("pylsp-venv") @@ -612,33 +754,29 @@ impl LspAdapter for PyLspAdapter { async fn check_if_user_installed( &self, - _: &dyn LspAdapterDelegate, - _: &AsyncAppContext, + delegate: &dyn LspAdapterDelegate, + toolchains: Arc, + cx: &AsyncAppContext, ) -> Option { - // We don't support user-provided pylsp, as global packages are discouraged in Python ecosystem. - None + let venv = toolchains + .active_toolchain( + delegate.worktree_id(), + LanguageName::new("Python"), + &mut cx.clone(), + ) + .await?; + let pylsp_path = Path::new(venv.path.as_ref()).parent()?.join("pylsp"); + pylsp_path.exists().then(|| LanguageServerBinary { + path: venv.path.to_string().into(), + arguments: vec![pylsp_path.into()], + env: None, + }) } async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, ) -> Result> { - // let uri = "https://pypi.org/pypi/python-lsp-server/json"; - // let mut root_manifest = delegate - // .http_client() - // .get(&uri, Default::default(), true) - // .await?; - // let mut body = Vec::new(); - // root_manifest.body_mut().read_to_end(&mut body).await?; - // let as_str = String::from_utf8(body)?; - // let json = serde_json::Value::from_str(&as_str)?; - // let latest_version = json - // .get("info") - // .and_then(|info| info.get("version")) - // .and_then(|version| version.as_str().map(ToOwned::to_owned)) - // .ok_or_else(|| { - // anyhow!("PyPI response did not contain version info for python-language-server") - // })?; Ok(Box::new(()) as Box<_>) } @@ -651,7 +789,7 @@ impl LspAdapter for PyLspAdapter { let venv = self.base_venv(delegate).await.map_err(|e| anyhow!(e))?; let pip_path = venv.join("bin").join("pip3"); ensure!( - Command::new(pip_path.as_path()) + util::command::new_smol_command(pip_path.as_path()) .arg("install") .arg("python-lsp-server") .output() @@ -661,7 +799,7 @@ impl LspAdapter for PyLspAdapter { "python-lsp-server installation failed" ); ensure!( - Command::new(pip_path.as_path()) + util::command::new_smol_command(pip_path.as_path()) .arg("install") .arg("python-lsp-server[all]") .output() @@ -671,7 +809,7 @@ impl LspAdapter for PyLspAdapter { "python-lsp-server[all] installation failed" ); ensure!( - Command::new(pip_path) + util::command::new_smol_command(pip_path) .arg("install") .arg("pylsp-mypy") .output() @@ -776,13 +914,17 @@ impl LspAdapter for PyLspAdapter { .unwrap_or_else(|| { json!({ "plugins": { - "rope_autoimport": {"enabled": true}, - "mypy": {"enabled": true} - } + "pycodestyle": {"enabled": false}, + "rope_autoimport": {"enabled": true, "memory": true}, + "pylsp_mypy": {"enabled": false} + }, + "rope": { + "ropeFolder": null + }, }) }); - // If python.pythonPath is not set in user config, do so using our toolchain picker. + // If user did not explicitly modify their python venv, use one from picker. if let Some(toolchain) = toolchain { if user_settings.is_null() { user_settings = Value::Object(serde_json::Map::default()); @@ -798,23 +940,22 @@ impl LspAdapter for PyLspAdapter { .or_insert(Value::Object(serde_json::Map::default())) .as_object_mut() { - jedi.insert( - "environment".to_string(), - Value::String(toolchain.path.clone().into()), - ); + jedi.entry("environment".to_string()) + .or_insert_with(|| Value::String(toolchain.path.clone().into())); } if let Some(pylint) = python - .entry("mypy") + .entry("pylsp_mypy") .or_insert(Value::Object(serde_json::Map::default())) .as_object_mut() { - pylint.insert( - "overrides".to_string(), + pylint.entry("overrides".to_string()).or_insert_with(|| { Value::Array(vec![ Value::String("--python-executable".into()), Value::String(toolchain.path.into()), - ]), - ); + Value::String("--cache-dir=/dev/null".into()), + Value::Bool(true), + ]) + }); } } } diff --git a/crates/languages/src/python/highlights.scm b/crates/languages/src/python/highlights.scm index e5f1b4d423..98ed203969 100644 --- a/crates/languages/src/python/highlights.scm +++ b/crates/languages/src/python/highlights.scm @@ -5,6 +5,14 @@ ; Type alias (type_alias_statement "type" @keyword) +; Identifier naming conventions + +((identifier) @type.class + (#match? @type.class "^[A-Z]")) + +((identifier) @constant + (#match? @constant "^_*[A-Z][A-Z\\d_]*$")) + ; TypeVar with constraints in type parameters (type (tuple (identifier) @type) @@ -12,25 +20,28 @@ ; Function calls -(decorator) @function +(decorator + "@" @punctuation.special + (identifier) @function.decorator) (call - function: (attribute attribute: (identifier) @function.method)) + function: (attribute attribute: (identifier) @function.method.call)) (call - function: (identifier) @function) + function: (identifier) @function.call) -; Function definitions +; Function and class definitions (function_definition - name: (identifier) @function) + name: (identifier) @function.definition) -; Identifier naming conventions +; Class definitions and calling: needs to come after the regex matching above -((identifier) @type - (#match? @type "^[A-Z]")) +(class_definition + name: (identifier) @type.class.definition) -((identifier) @constant - (#match? @constant "^_*[A-Z][A-Z\\d_]*$")) +(call + function: (identifier) @type.class.call + (#match? @type.class.call "^[A-Z][A-Z0-9_]*[a-z]")) ; Builtin functions @@ -46,6 +57,7 @@ (none) (true) (false) + (ellipsis) ] @constant.builtin [ @@ -58,7 +70,7 @@ [ (parameters (identifier) @variable.special) (attribute (identifier) @variable.special) - (#match? @variable.special "^self$") + (#match? @variable.special "^self|cls$") ] (comment) @comment @@ -84,7 +96,35 @@ "def" name: (_) (parameters)? - body: (block (expression_statement (string) @string.doc))) + body: (block . (expression_statement (string) @string.doc))) + +(class_definition + body: (block + . (comment) @comment* + . (expression_statement (string) @string.doc))) + +(module + . (comment) @comment* + . (expression_statement (string) @string.doc)) + +(module + (expression_statement (assignment)) + . (expression_statement (string) @string.doc)) + +(class_definition + body: (block + (expression_statement (assignment)) + . (expression_statement (string) @string.doc))) + +(class_definition + body: (block + (function_definition + name: (identifier) @function.method.constructor + (#eq? @function.method.constructor "__init__") + body: (block + (expression_statement (assignment)) + . (expression_statement (string) @string.doc))))) + [ "-" diff --git a/crates/languages/src/python/runnables.scm b/crates/languages/src/python/runnables.scm index b9bc5e9bf2..31994dfa2c 100644 --- a/crates/languages/src/python/runnables.scm +++ b/crates/languages/src/python/runnables.scm @@ -29,3 +29,42 @@ ) ) ) + +; pytest functions +( + (module + (function_definition + name: (identifier) @run @_pytest_method_name + (#match? @_pytest_method_name "^test_") + ) @python-pytest-method + ) + (#set! tag python-pytest-method) +) + +; pytest classes +( + (module + (class_definition + name: (identifier) @run @_pytest_class_name + (#match? @_pytest_class_name "^Test") + ) + (#set! tag python-pytest-class) + ) +) + +; pytest class methods +( + (module + (class_definition + name: (identifier) @_pytest_class_name + (#match? @_pytest_class_name "^Test") + body: (block + (function_definition + name: (identifier) @run @_pytest_method_name + (#match? @_pytest_method_name "^test") + ) @python-pytest-method + (#set! tag python-pytest-method) + ) + ) + ) +) diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 730f20b134..25cddae5a6 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -14,8 +14,7 @@ use std::{ any::Any, borrow::Cow, path::{Path, PathBuf}, - sync::Arc, - sync::LazyLock, + sync::{Arc, LazyLock}, }; use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName}; use util::{fs::remove_matching, maybe, ResultExt}; @@ -77,6 +76,7 @@ impl LspAdapter for RustLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, + _: Arc, _: &AsyncAppContext, ) -> Option { let path = delegate.which("rust-analyzer".as_ref()).await?; @@ -639,7 +639,7 @@ fn package_name_and_bin_name_from_abs_path( abs_path: &Path, project_env: Option<&HashMap>, ) -> Option<(String, String)> { - let mut command = std::process::Command::new("cargo"); + let mut command = util::command::new_std_command("cargo"); if let Some(envs) = project_env { command.envs(envs); } @@ -685,11 +685,10 @@ fn human_readable_package_name( package_directory: &Path, project_env: Option<&HashMap>, ) -> Option { - let mut command = std::process::Command::new("cargo"); + let mut command = util::command::new_std_command("cargo"); if let Some(envs) = project_env { command.envs(envs); } - let pkgid = String::from_utf8( command .current_dir(package_directory) diff --git a/crates/languages/src/tsx/highlights.scm b/crates/languages/src/tsx/highlights.scm index bbdd83bb4d..26cf5c207b 100644 --- a/crates/languages/src/tsx/highlights.scm +++ b/crates/languages/src/tsx/highlights.scm @@ -155,6 +155,13 @@ "}" ] @punctuation.bracket +(ternary_expression + [ + "?" + ":" + ] @operator +) + [ "as" "async" diff --git a/crates/languages/src/typescript/highlights.scm b/crates/languages/src/typescript/highlights.scm index eedcf79aed..f7b893da7a 100644 --- a/crates/languages/src/typescript/highlights.scm +++ b/crates/languages/src/typescript/highlights.scm @@ -156,6 +156,13 @@ "}" ] @punctuation.bracket +(ternary_expression + [ + "?" + ":" + ] @operator +) + [ "as" "async" diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index 0ad9158003..e44e4e295f 100644 --- a/crates/languages/src/vtsls.rs +++ b/crates/languages/src/vtsls.rs @@ -77,6 +77,7 @@ impl LspAdapter for VtslsLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, + _: Arc, _: &AsyncAppContext, ) -> Option { let env = delegate.shell_env().await; diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml index 3460bf34dd..f06173ac1b 100644 --- a/crates/lsp/Cargo.toml +++ b/crates/lsp/Cargo.toml @@ -32,9 +32,6 @@ smol.workspace = true util.workspace = true release_channel.workspace = true -[target.'cfg(windows)'.dependencies] -windows.workspace = true - [dev-dependencies] async-pipe.workspace = true ctor.workspace = true diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index ca09ef4d1f..87c04030bd 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -19,12 +19,9 @@ use serde_json::{json, value::RawValue, Value}; use smol::{ channel, io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, - process::{self, Child}, + process::Child, }; -#[cfg(target_os = "windows")] -use smol::process::windows::CommandExt; - use std::{ ffi::{OsStr, OsString}, fmt, @@ -346,23 +343,21 @@ impl LanguageServer { &binary.arguments ); - let mut command = process::Command::new(&binary.path); - command + let mut server = util::command::new_smol_command(&binary.path) .current_dir(working_dir) .args(&binary.arguments) .envs(binary.env.unwrap_or_default()) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) - .kill_on_drop(true); - #[cfg(windows)] - command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); - let mut server = command.spawn().with_context(|| { - format!( - "failed to spawn command. path: {:?}, working directory: {:?}, args: {:?}", - binary.path, working_dir, &binary.arguments - ) - })?; + .kill_on_drop(true) + .spawn() + .with_context(|| { + format!( + "failed to spawn command. path: {:?}, working directory: {:?}, args: {:?}", + binary.path, working_dir, &binary.arguments + ) + })?; let stdin = server.stdin.take().unwrap(); let stdout = server.stdout.take().unwrap(); @@ -762,6 +757,7 @@ impl LanguageServer { }), experimental: Some(json!({ "serverStatusNotification": true, + "localDocs": true, })), window: Some(WindowClientCapabilities { work_done_progress: Some(true), diff --git a/crates/markdown_preview/src/markdown_elements.rs b/crates/markdown_preview/src/markdown_elements.rs index 8423e4ec82..ff43fab08a 100644 --- a/crates/markdown_preview/src/markdown_elements.rs +++ b/crates/markdown_preview/src/markdown_elements.rs @@ -13,7 +13,7 @@ pub enum ParsedMarkdownElement { BlockQuote(ParsedMarkdownBlockQuote), CodeBlock(ParsedMarkdownCodeBlock), /// A paragraph of text and other inline elements. - Paragraph(ParsedMarkdownText), + Paragraph(MarkdownParagraph), HorizontalRule(Range), } @@ -25,7 +25,13 @@ impl ParsedMarkdownElement { Self::Table(table) => table.source_range.clone(), Self::BlockQuote(block_quote) => block_quote.source_range.clone(), Self::CodeBlock(code_block) => code_block.source_range.clone(), - Self::Paragraph(text) => text.source_range.clone(), + Self::Paragraph(text) => match &text[0] { + MarkdownParagraphChunk::Text(t) => t.source_range.clone(), + MarkdownParagraphChunk::Image(image) => match image { + Image::Web { source_range, .. } => source_range.clone(), + Image::Path { source_range, .. } => source_range.clone(), + }, + }, Self::HorizontalRule(range) => range.clone(), } } @@ -35,6 +41,15 @@ impl ParsedMarkdownElement { } } +pub type MarkdownParagraph = Vec; + +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] +pub enum MarkdownParagraphChunk { + Text(ParsedMarkdownText), + Image(Image), +} + #[derive(Debug)] #[cfg_attr(test, derive(PartialEq))] pub struct ParsedMarkdown { @@ -73,7 +88,7 @@ pub struct ParsedMarkdownCodeBlock { pub struct ParsedMarkdownHeading { pub source_range: Range, pub level: HeadingLevel, - pub contents: ParsedMarkdownText, + pub contents: MarkdownParagraph, } #[derive(Debug, PartialEq)] @@ -107,7 +122,7 @@ pub enum ParsedMarkdownTableAlignment { #[derive(Debug)] #[cfg_attr(test, derive(PartialEq))] pub struct ParsedMarkdownTableRow { - pub children: Vec, + pub children: Vec, } impl Default for ParsedMarkdownTableRow { @@ -123,7 +138,7 @@ impl ParsedMarkdownTableRow { } } - pub fn with_children(children: Vec) -> Self { + pub fn with_children(children: Vec) -> Self { Self { children } } } @@ -135,7 +150,7 @@ pub struct ParsedMarkdownBlockQuote { pub children: Vec, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ParsedMarkdownText { /// Where the text is located in the source Markdown document. pub source_range: Range, @@ -266,10 +281,112 @@ impl Display for Link { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Link::Web { url } => write!(f, "{}", url), - Link::Path { - display_path, - path: _, - } => write!(f, "{}", display_path.display()), + Link::Path { display_path, .. } => write!(f, "{}", display_path.display()), + } + } +} + +/// A Markdown Image +#[derive(Debug, Clone)] +#[cfg_attr(test, derive(PartialEq))] +pub enum Image { + Web { + source_range: Range, + /// The URL of the Image. + url: String, + /// Link URL if exists. + link: Option, + /// alt text if it exists + alt_text: Option, + }, + /// Image path on the filesystem. + Path { + source_range: Range, + /// The path as provided in the Markdown document. + display_path: PathBuf, + /// The absolute path to the item. + path: PathBuf, + /// Link URL if exists. + link: Option, + /// alt text if it exists + alt_text: Option, + }, +} + +impl Image { + pub fn identify( + source_range: Range, + file_location_directory: Option, + text: String, + link: Option, + ) -> Option { + if text.starts_with("http") { + return Some(Image::Web { + source_range, + url: text, + link, + alt_text: None, + }); + } + let path = PathBuf::from(&text); + if path.is_absolute() { + return Some(Image::Path { + source_range, + display_path: path.clone(), + path, + link, + alt_text: None, + }); + } + if let Some(file_location_directory) = file_location_directory { + let display_path = path; + let path = file_location_directory.join(text); + return Some(Image::Path { + source_range, + display_path, + path, + link, + alt_text: None, + }); + } + None + } + + pub fn with_alt_text(&self, alt_text: ParsedMarkdownText) -> Self { + match self { + Image::Web { + ref source_range, + ref url, + ref link, + .. + } => Image::Web { + source_range: source_range.clone(), + url: url.clone(), + link: link.clone(), + alt_text: Some(alt_text), + }, + Image::Path { + ref source_range, + ref display_path, + ref path, + ref link, + .. + } => Image::Path { + source_range: source_range.clone(), + display_path: display_path.clone(), + path: path.clone(), + link: link.clone(), + alt_text: Some(alt_text), + }, + } + } +} + +impl Display for Image { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Image::Web { url, .. } => write!(f, "{}", url), + Image::Path { display_path, .. } => write!(f, "{}", display_path.display()), } } } diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index d514b89e52..211cca2494 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -4,7 +4,7 @@ use collections::FxHashMap; use gpui::FontWeight; use language::LanguageRegistry; use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd}; -use std::{ops::Range, path::PathBuf, sync::Arc}; +use std::{ops::Range, path::PathBuf, sync::Arc, vec}; pub async fn parse_markdown( markdown_input: &str, @@ -101,11 +101,11 @@ impl<'a> MarkdownParser<'a> { | Event::Code(_) | Event::Html(_) | Event::FootnoteReference(_) - | Event::Start(Tag::Link { link_type: _, dest_url: _, title: _, id: _ }) + | Event::Start(Tag::Link { .. }) | Event::Start(Tag::Emphasis) | Event::Start(Tag::Strong) | Event::Start(Tag::Strikethrough) - | Event::Start(Tag::Image { link_type: _, dest_url: _, title: _, id: _ }) => { + | Event::Start(Tag::Image { .. }) => { true } _ => false, @@ -134,12 +134,7 @@ impl<'a> MarkdownParser<'a> { let text = self.parse_text(false, Some(source_range)); Some(vec![ParsedMarkdownElement::Paragraph(text)]) } - Tag::Heading { - level, - id: _, - classes: _, - attrs: _, - } => { + Tag::Heading { level, .. } => { let level = *level; self.cursor += 1; let heading = self.parse_heading(level); @@ -194,22 +189,23 @@ impl<'a> MarkdownParser<'a> { &mut self, should_complete_on_soft_break: bool, source_range: Option>, - ) -> ParsedMarkdownText { + ) -> MarkdownParagraph { let source_range = source_range.unwrap_or_else(|| { self.current() .map(|(_, range)| range.clone()) .unwrap_or_default() }); + let mut markdown_text_like = Vec::new(); let mut text = String::new(); let mut bold_depth = 0; let mut italic_depth = 0; let mut strikethrough_depth = 0; let mut link: Option = None; + let mut image: Option = None; let mut region_ranges: Vec> = vec![]; let mut regions: Vec = vec![]; let mut highlights: Vec<(Range, MarkdownHighlight)> = vec![]; - let mut link_urls: Vec = vec![]; let mut link_ranges: Vec> = vec![]; @@ -225,8 +221,6 @@ impl<'a> MarkdownParser<'a> { if should_complete_on_soft_break { break; } - - // `Some text\nSome more text` should be treated as a single line. text.push(' '); } @@ -240,7 +234,6 @@ impl<'a> MarkdownParser<'a> { Event::Text(t) => { text.push_str(t.as_ref()); - let mut style = MarkdownHighlightStyle::default(); if bold_depth > 0 { @@ -299,7 +292,6 @@ impl<'a> MarkdownParser<'a> { url: link.as_str().to_string(), }), }); - last_link_len = end; } last_link_len @@ -316,13 +308,63 @@ impl<'a> MarkdownParser<'a> { } } if new_highlight { - highlights - .push((last_run_len..text.len(), MarkdownHighlight::Style(style))); + highlights.push(( + last_run_len..text.len(), + MarkdownHighlight::Style(style.clone()), + )); } } - } + if let Some(mut image) = image.clone() { + let is_valid_image = match image.clone() { + Image::Path { display_path, .. } => { + gpui::ImageSource::try_from(display_path).is_ok() + } + Image::Web { url, .. } => gpui::ImageSource::try_from(url).is_ok(), + }; + if is_valid_image { + text.truncate(text.len() - t.len()); + if !t.is_empty() { + let alt_text = ParsedMarkdownText { + source_range: source_range.clone(), + contents: t.to_string(), + highlights: highlights.clone(), + region_ranges: region_ranges.clone(), + regions: regions.clone(), + }; + image = image.with_alt_text(alt_text); + } else { + let alt_text = ParsedMarkdownText { + source_range: source_range.clone(), + contents: "img".to_string(), + highlights: highlights.clone(), + region_ranges: region_ranges.clone(), + regions: regions.clone(), + }; + image = image.with_alt_text(alt_text); + } + if !text.is_empty() { + let parsed_regions = + MarkdownParagraphChunk::Text(ParsedMarkdownText { + source_range: source_range.clone(), + contents: text.clone(), + highlights: highlights.clone(), + region_ranges: region_ranges.clone(), + regions: regions.clone(), + }); + text = String::new(); + highlights = vec![]; + region_ranges = vec![]; + regions = vec![]; + markdown_text_like.push(parsed_regions); + } - // Note: This event means "inline code" and not "code block" + let parsed_image = MarkdownParagraphChunk::Image(image.clone()); + markdown_text_like.push(parsed_image); + style = MarkdownHighlightStyle::default(); + } + style.underline = true; + }; + } Event::Code(t) => { text.push_str(t.as_ref()); region_ranges.push(prev_len..text.len()); @@ -336,46 +378,44 @@ impl<'a> MarkdownParser<'a> { }), )); } - regions.push(ParsedRegion { code: true, link: link.clone(), }); } - Event::Start(tag) => match tag { Tag::Emphasis => italic_depth += 1, Tag::Strong => bold_depth += 1, Tag::Strikethrough => strikethrough_depth += 1, - Tag::Link { - link_type: _, - dest_url, - title: _, - id: _, - } => { + Tag::Link { dest_url, .. } => { link = Link::identify( self.file_location_directory.clone(), dest_url.to_string(), ); } + Tag::Image { dest_url, .. } => { + image = Image::identify( + source_range.clone(), + self.file_location_directory.clone(), + dest_url.to_string(), + link.clone(), + ); + } _ => { break; } }, Event::End(tag) => match tag { - TagEnd::Emphasis => { - italic_depth -= 1; - } - TagEnd::Strong => { - bold_depth -= 1; - } - TagEnd::Strikethrough => { - strikethrough_depth -= 1; - } + TagEnd::Emphasis => italic_depth -= 1, + TagEnd::Strong => bold_depth -= 1, + TagEnd::Strikethrough => strikethrough_depth -= 1, TagEnd::Link => { link = None; } + TagEnd::Image => { + image = None; + } TagEnd::Paragraph => { self.cursor += 1; break; @@ -384,7 +424,6 @@ impl<'a> MarkdownParser<'a> { break; } }, - _ => { break; } @@ -392,14 +431,16 @@ impl<'a> MarkdownParser<'a> { self.cursor += 1; } - - ParsedMarkdownText { - source_range, - contents: text, - highlights, - regions, - region_ranges, + if !text.is_empty() { + markdown_text_like.push(MarkdownParagraphChunk::Text(ParsedMarkdownText { + source_range: source_range.clone(), + contents: text, + highlights, + regions, + region_ranges, + })); } + markdown_text_like } fn parse_heading(&mut self, level: pulldown_cmark::HeadingLevel) -> ParsedMarkdownHeading { @@ -708,7 +749,6 @@ impl<'a> MarkdownParser<'a> { } } } - let highlights = if let Some(language) = &language { if let Some(registry) = &self.language_registry { let rope: language::Rope = code.as_str().into(); @@ -735,10 +775,14 @@ impl<'a> MarkdownParser<'a> { #[cfg(test)] mod tests { + use core::panic; + use super::*; use gpui::BackgroundExecutor; - use language::{tree_sitter_rust, HighlightId, Language, LanguageConfig, LanguageMatcher}; + use language::{ + tree_sitter_rust, HighlightId, Language, LanguageConfig, LanguageMatcher, LanguageRegistry, + }; use pretty_assertions::assert_eq; use ParsedMarkdownListItemType::*; @@ -810,20 +854,29 @@ mod tests { assert_eq!(parsed.children.len(), 1); assert_eq!( parsed.children[0], - ParsedMarkdownElement::Paragraph(ParsedMarkdownText { - source_range: 0..35, - contents: "Some bostrikethroughld text".to_string(), - highlights: Vec::new(), - region_ranges: Vec::new(), - regions: Vec::new(), - }) + ParsedMarkdownElement::Paragraph(vec![MarkdownParagraphChunk::Text( + ParsedMarkdownText { + source_range: 0..35, + contents: "Some bostrikethroughld text".to_string(), + highlights: Vec::new(), + region_ranges: Vec::new(), + regions: Vec::new(), + } + )]) ); - let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { + let new_text = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { text } else { panic!("Expected a paragraph"); }; + + let paragraph = if let MarkdownParagraphChunk::Text(text) = &new_text[0] { + text + } else { + panic!("Expected a text"); + }; + assert_eq!( paragraph.highlights, vec![ @@ -871,6 +924,11 @@ mod tests { parsed.children, vec![p("Checkout this https://zed.dev link", 0..34)] ); + } + + #[gpui::test] + async fn test_image_links_detection() { + let parsed = parse("![test](https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png)").await; let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { text @@ -878,25 +936,22 @@ mod tests { panic!("Expected a paragraph"); }; assert_eq!( - paragraph.highlights, - vec![( - 14..29, - MarkdownHighlight::Style(MarkdownHighlightStyle { - underline: true, - ..Default::default() - }), - )] + paragraph[0], + MarkdownParagraphChunk::Image(Image::Web { + source_range: 0..111, + url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(), + link: None, + alt_text: Some( + ParsedMarkdownText { + source_range: 0..111, + contents: "test".to_string(), + highlights: vec![], + region_ranges: vec![], + regions: vec![], + }, + ), + },) ); - assert_eq!( - paragraph.regions, - vec![ParsedRegion { - code: false, - link: Some(Link::Web { - url: "https://zed.dev".to_string() - }), - }] - ); - assert_eq!(paragraph.region_ranges, vec![14..29]); } #[gpui::test] @@ -1169,7 +1224,7 @@ Some other content vec![ list_item(0..8, 1, Unordered, vec![p("code", 2..8)]), list_item(9..19, 1, Unordered, vec![p("bold", 11..19)]), - list_item(20..49, 1, Unordered, vec![p("link", 22..49)],) + list_item(20..49, 1, Unordered, vec![p("link", 22..49)],), ], ); } @@ -1312,7 +1367,7 @@ fn main() { )) } - fn h1(contents: ParsedMarkdownText, source_range: Range) -> ParsedMarkdownElement { + fn h1(contents: MarkdownParagraph, source_range: Range) -> ParsedMarkdownElement { ParsedMarkdownElement::Heading(ParsedMarkdownHeading { source_range, level: HeadingLevel::H1, @@ -1320,7 +1375,7 @@ fn main() { }) } - fn h2(contents: ParsedMarkdownText, source_range: Range) -> ParsedMarkdownElement { + fn h2(contents: MarkdownParagraph, source_range: Range) -> ParsedMarkdownElement { ParsedMarkdownElement::Heading(ParsedMarkdownHeading { source_range, level: HeadingLevel::H2, @@ -1328,7 +1383,7 @@ fn main() { }) } - fn h3(contents: ParsedMarkdownText, source_range: Range) -> ParsedMarkdownElement { + fn h3(contents: MarkdownParagraph, source_range: Range) -> ParsedMarkdownElement { ParsedMarkdownElement::Heading(ParsedMarkdownHeading { source_range, level: HeadingLevel::H3, @@ -1340,14 +1395,14 @@ fn main() { ParsedMarkdownElement::Paragraph(text(contents, source_range)) } - fn text(contents: &str, source_range: Range) -> ParsedMarkdownText { - ParsedMarkdownText { + fn text(contents: &str, source_range: Range) -> MarkdownParagraph { + vec![MarkdownParagraphChunk::Text(ParsedMarkdownText { highlights: Vec::new(), region_ranges: Vec::new(), regions: Vec::new(), source_range, contents: contents.to_string(), - } + })] } fn block_quote( @@ -1401,7 +1456,7 @@ fn main() { } } - fn row(children: Vec) -> ParsedMarkdownTableRow { + fn row(children: Vec) -> ParsedMarkdownTableRow { ParsedMarkdownTableRow { children } } diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index f38e1c49b5..6140372e0b 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -1,29 +1,33 @@ use crate::markdown_elements::{ - HeadingLevel, Link, ParsedMarkdown, ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock, - ParsedMarkdownElement, ParsedMarkdownHeading, ParsedMarkdownListItem, - ParsedMarkdownListItemType, ParsedMarkdownTable, ParsedMarkdownTableAlignment, - ParsedMarkdownTableRow, ParsedMarkdownText, + HeadingLevel, Image, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedMarkdown, + ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock, ParsedMarkdownElement, + ParsedMarkdownHeading, ParsedMarkdownListItem, ParsedMarkdownListItemType, ParsedMarkdownTable, + ParsedMarkdownTableAlignment, ParsedMarkdownTableRow, ParsedMarkdownText, }; use gpui::{ - div, px, rems, AbsoluteLength, AnyElement, ClipboardItem, DefiniteLength, Div, Element, - ElementId, HighlightStyle, Hsla, InteractiveText, IntoElement, Keystroke, Length, Modifiers, - ParentElement, SharedString, Styled, StyledText, TextStyle, WeakView, WindowContext, + div, img, px, rems, AbsoluteLength, AnyElement, ClipboardItem, DefiniteLength, Div, Element, + ElementId, HighlightStyle, Hsla, ImageSource, InteractiveText, IntoElement, Keystroke, Length, + Modifiers, ParentElement, Resource, SharedString, Styled, StyledText, TextStyle, WeakView, + WindowContext, }; use settings::Settings; use std::{ ops::{Mul, Range}, + path::Path, sync::Arc, + vec, }; use theme::{ActiveTheme, SyntaxTheme, ThemeSettings}; use ui::{ h_flex, relative, v_flex, Checkbox, Clickable, FluentBuilder, IconButton, IconName, IconSize, - InteractiveElement, LinkPreview, Selection, StatefulInteractiveElement, StyledExt, Tooltip, - VisibleOnHover, + InteractiveElement, LinkPreview, Selection, StatefulInteractiveElement, StyledExt, StyledImage, + Tooltip, VisibleOnHover, }; use workspace::Workspace; type CheckboxClickedCallback = Arc, &mut WindowContext)>>; +#[derive(Clone)] pub struct RenderContext { workspace: Option>, next_id: usize, @@ -153,7 +157,7 @@ fn render_markdown_heading(parsed: &ParsedMarkdownHeading, cx: &mut RenderContex .text_color(color) .pt(rems(0.15)) .pb_1() - .child(render_markdown_text(&parsed.contents, cx)) + .children(render_markdown_text(&parsed.contents, cx)) .whitespace_normal() .into_any() } @@ -206,7 +210,7 @@ fn render_markdown_list_item( let secondary_modifier = Keystroke { key: "".to_string(), modifiers: Modifiers::secondary_key(), - ime_key: None, + key_char: None, }; Tooltip::text( format!("{}-click to toggle the checkbox", secondary_modifier), @@ -231,17 +235,29 @@ fn render_markdown_list_item( cx.with_common_p(item).into_any() } +fn paragraph_len(paragraphs: &MarkdownParagraph) -> usize { + paragraphs + .iter() + .map(|paragraph| match paragraph { + MarkdownParagraphChunk::Text(text) => text.contents.len(), + // TODO: Scale column width based on image size + MarkdownParagraphChunk::Image(_) => 1, + }) + .sum() +} + fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -> AnyElement { let mut max_lengths: Vec = vec![0; parsed.header.children.len()]; for (index, cell) in parsed.header.children.iter().enumerate() { - let length = cell.contents.len(); + let length = paragraph_len(&cell); max_lengths[index] = length; } for row in &parsed.body { for (index, cell) in row.children.iter().enumerate() { - let length = cell.contents.len(); + let length = paragraph_len(&cell); + if length > max_lengths[index] { max_lengths[index] = length; } @@ -307,11 +323,10 @@ fn render_markdown_table_row( }; let max_width = max_column_widths.get(index).unwrap_or(&0.0); - let mut cell = container .w(Length::Definite(relative(*max_width))) .h_full() - .child(contents) + .children(contents) .px_2() .py_1() .border_color(cx.border_color); @@ -398,18 +413,219 @@ fn render_markdown_code_block( .into_any() } -fn render_markdown_paragraph(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> AnyElement { +fn render_markdown_paragraph(parsed: &MarkdownParagraph, cx: &mut RenderContext) -> AnyElement { cx.with_common_p(div()) - .child(render_markdown_text(parsed, cx)) + .children(render_markdown_text(parsed, cx)) + .flex() .into_any_element() } -fn render_markdown_text(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> AnyElement { - let element_id = cx.next_id(&parsed.source_range); +fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext) -> Vec { + let mut any_element = vec![]; + // these values are cloned in-order satisfy borrow checker + let syntax_theme = cx.syntax_theme.clone(); + let workspace_clone = cx.workspace.clone(); + let code_span_bg_color = cx.code_span_background_color; + let text_style = cx.text_style.clone(); + + for parsed_region in parsed_new { + match parsed_region { + MarkdownParagraphChunk::Text(parsed) => { + let element_id = cx.next_id(&parsed.source_range); + + let highlights = gpui::combine_highlights( + parsed.highlights.iter().filter_map(|(range, highlight)| { + highlight + .to_highlight_style(&syntax_theme) + .map(|style| (range.clone(), style)) + }), + parsed.regions.iter().zip(&parsed.region_ranges).filter_map( + |(region, range)| { + if region.code { + Some(( + range.clone(), + HighlightStyle { + background_color: Some(code_span_bg_color), + ..Default::default() + }, + )) + } else { + None + } + }, + ), + ); + let mut links = Vec::new(); + let mut link_ranges = Vec::new(); + for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) { + if let Some(link) = region.link.clone() { + links.push(link); + link_ranges.push(range.clone()); + } + } + let workspace = workspace_clone.clone(); + let element = div() + .child( + InteractiveText::new( + element_id, + StyledText::new(parsed.contents.clone()) + .with_highlights(&text_style, highlights), + ) + .tooltip({ + let links = links.clone(); + let link_ranges = link_ranges.clone(); + move |idx, cx| { + for (ix, range) in link_ranges.iter().enumerate() { + if range.contains(&idx) { + return Some(LinkPreview::new(&links[ix].to_string(), cx)); + } + } + None + } + }) + .on_click( + link_ranges, + move |clicked_range_ix, window_cx| match &links[clicked_range_ix] { + Link::Web { url } => window_cx.open_url(url), + Link::Path { path, .. } => { + if let Some(workspace) = &workspace { + _ = workspace.update(window_cx, |workspace, cx| { + workspace + .open_abs_path(path.clone(), false, cx) + .detach(); + }); + } + } + }, + ), + ) + .into_any(); + any_element.push(element); + } + + MarkdownParagraphChunk::Image(image) => { + let (link, source_range, image_source, alt_text) = match image { + Image::Web { + link, + source_range, + url, + alt_text, + } => ( + link, + source_range, + Resource::Uri(url.clone().into()), + alt_text, + ), + Image::Path { + link, + source_range, + path, + alt_text, + .. + } => { + let image_path = Path::new(path.to_str().unwrap()); + ( + link, + source_range, + Resource::Path(Arc::from(image_path)), + alt_text, + ) + } + }; + + let element_id = cx.next_id(source_range); + + match link { + None => { + let fallback_workspace = workspace_clone.clone(); + let fallback_syntax_theme = syntax_theme.clone(); + let fallback_text_style = text_style.clone(); + let fallback_alt_text = alt_text.clone(); + let element_id_new = element_id.clone(); + let element = div() + .child(img(ImageSource::Resource(image_source)).with_fallback({ + move || { + fallback_text( + fallback_alt_text.clone().unwrap(), + element_id.clone(), + &fallback_syntax_theme, + code_span_bg_color, + fallback_workspace.clone(), + &fallback_text_style, + ) + } + })) + .id(element_id_new) + .into_any(); + any_element.push(element); + } + Some(link) => { + let link_click = link.clone(); + let link_tooltip = link.clone(); + let fallback_workspace = workspace_clone.clone(); + let fallback_syntax_theme = syntax_theme.clone(); + let fallback_text_style = text_style.clone(); + let fallback_alt_text = alt_text.clone(); + let element_id_new = element_id.clone(); + let image_element = div() + .child(img(ImageSource::Resource(image_source)).with_fallback({ + move || { + fallback_text( + fallback_alt_text.clone().unwrap(), + element_id.clone(), + &fallback_syntax_theme, + code_span_bg_color, + fallback_workspace.clone(), + &fallback_text_style, + ) + } + })) + .id(element_id_new) + .tooltip(move |cx| LinkPreview::new(&link_tooltip.to_string(), cx)) + .on_click({ + let workspace = workspace_clone.clone(); + move |_event, window_cx| match &link_click { + Link::Web { url } => window_cx.open_url(url), + Link::Path { path, .. } => { + if let Some(workspace) = &workspace { + _ = workspace.update(window_cx, |workspace, cx| { + workspace + .open_abs_path(path.clone(), false, cx) + .detach(); + }); + } + } + } + }) + .into_any(); + any_element.push(image_element); + } + } + } + } + } + + any_element +} + +fn render_markdown_rule(cx: &mut RenderContext) -> AnyElement { + let rule = div().w_full().h(px(2.)).bg(cx.border_color); + div().pt_3().pb_3().child(rule).into_any() +} + +fn fallback_text( + parsed: ParsedMarkdownText, + source_range: ElementId, + syntax_theme: &theme::SyntaxTheme, + code_span_bg_color: Hsla, + workspace: Option>, + text_style: &TextStyle, +) -> AnyElement { + let element_id = source_range; let highlights = gpui::combine_highlights( parsed.highlights.iter().filter_map(|(range, highlight)| { - let highlight = highlight.to_highlight_style(&cx.syntax_theme)?; + let highlight = highlight.to_highlight_style(syntax_theme)?; Some((range.clone(), highlight)) }), parsed @@ -421,7 +637,7 @@ fn render_markdown_text(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> Some(( range.clone(), HighlightStyle { - background_color: Some(cx.code_span_background_color), + background_color: Some(code_span_bg_color), ..Default::default() }, )) @@ -430,7 +646,6 @@ fn render_markdown_text(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> } }), ); - let mut links = Vec::new(); let mut link_ranges = Vec::new(); for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) { @@ -439,45 +654,38 @@ fn render_markdown_text(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> link_ranges.push(range.clone()); } } - - let workspace = cx.workspace.clone(); - - InteractiveText::new( - element_id, - StyledText::new(parsed.contents.clone()).with_highlights(&cx.text_style, highlights), - ) - .tooltip({ - let links = links.clone(); - let link_ranges = link_ranges.clone(); - move |idx, cx| { - for (ix, range) in link_ranges.iter().enumerate() { - if range.contains(&idx) { - return Some(LinkPreview::new(&links[ix].to_string(), cx)); + let element = div() + .child( + InteractiveText::new( + element_id, + StyledText::new(parsed.contents.clone()).with_highlights(text_style, highlights), + ) + .tooltip({ + let links = links.clone(); + let link_ranges = link_ranges.clone(); + move |idx, cx| { + for (ix, range) in link_ranges.iter().enumerate() { + if range.contains(&idx) { + return Some(LinkPreview::new(&links[ix].to_string(), cx)); + } + } + None } - } - None - } - }) - .on_click( - link_ranges, - move |clicked_range_ix, window_cx| match &links[clicked_range_ix] { - Link::Web { url } => window_cx.open_url(url), - Link::Path { - path, - display_path: _, - } => { - if let Some(workspace) = &workspace { - _ = workspace.update(window_cx, |workspace, cx| { - workspace.open_abs_path(path.clone(), false, cx).detach(); - }); - } - } - }, - ) - .into_any_element() -} - -fn render_markdown_rule(cx: &mut RenderContext) -> AnyElement { - let rule = div().w_full().h(px(2.)).bg(cx.border_color); - div().pt_3().pb_3().child(rule).into_any() + }) + .on_click( + link_ranges, + move |clicked_range_ix, window_cx| match &links[clicked_range_ix] { + Link::Web { url } => window_cx.open_url(url), + Link::Path { path, .. } => { + if let Some(workspace) = &workspace { + _ = workspace.update(window_cx, |workspace, cx| { + workspace.open_abs_path(path.clone(), false, cx).detach(); + }); + } + } + }, + ), + ) + .into_any(); + return element; } diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 5a746c42ba..3aa4fb7cce 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -10,9 +10,9 @@ use itertools::Itertools; use language::{ language_settings::{language_settings, LanguageSettings}, AutoindentMode, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability, CharClassifier, - CharKind, Chunk, CursorShape, DiagnosticEntry, File, IndentGuide, IndentSize, Language, - LanguageScope, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem, Point, PointUtf16, Selection, - TextDimension, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, + CharKind, Chunk, CursorShape, DiagnosticEntry, DiskState, File, IndentGuide, IndentSize, + Language, LanguageScope, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem, Point, PointUtf16, + Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped, }; use smallvec::SmallVec; @@ -186,6 +186,7 @@ pub struct MultiBufferSnapshot { non_text_state_update_count: usize, edit_count: usize, is_dirty: bool, + has_deleted_file: bool, has_conflict: bool, show_headers: bool, } @@ -494,6 +495,10 @@ impl MultiBuffer { self.read(cx).is_dirty() } + pub fn has_deleted_file(&self, cx: &AppContext) -> bool { + self.read(cx).has_deleted_file() + } + pub fn has_conflict(&self, cx: &AppContext) -> bool { self.read(cx).has_conflict() } @@ -1419,6 +1424,7 @@ impl MultiBuffer { snapshot.excerpts = Default::default(); snapshot.trailing_excerpt_update_count += 1; snapshot.is_dirty = false; + snapshot.has_deleted_file = false; snapshot.has_conflict = false; self.subscriptions.publish_mut([Edit { @@ -2003,6 +2009,7 @@ impl MultiBuffer { let mut excerpts_to_edit = Vec::new(); let mut non_text_state_updated = false; let mut is_dirty = false; + let mut has_deleted_file = false; let mut has_conflict = false; let mut edited = false; let mut buffers = self.buffers.borrow_mut(); @@ -2028,6 +2035,9 @@ impl MultiBuffer { edited |= buffer_edited; non_text_state_updated |= buffer_non_text_state_updated; is_dirty |= buffer.is_dirty(); + has_deleted_file |= buffer + .file() + .map_or(false, |file| file.disk_state() == DiskState::Deleted); has_conflict |= buffer.has_conflict(); } if edited { @@ -2037,6 +2047,7 @@ impl MultiBuffer { snapshot.non_text_state_update_count += 1; } snapshot.is_dirty = is_dirty; + snapshot.has_deleted_file = has_deleted_file; snapshot.has_conflict = has_conflict; excerpts_to_edit.sort_unstable_by_key(|(locator, _, _)| *locator); @@ -3701,6 +3712,10 @@ impl MultiBufferSnapshot { self.is_dirty } + pub fn has_deleted_file(&self) -> bool { + self.has_deleted_file + } + pub fn has_conflict(&self) -> bool { self.has_conflict } diff --git a/crates/node_runtime/Cargo.toml b/crates/node_runtime/Cargo.toml index d852b7ebdf..20b6be407f 100644 --- a/crates/node_runtime/Cargo.toml +++ b/crates/node_runtime/Cargo.toml @@ -37,7 +37,6 @@ which.workspace = true [target.'cfg(windows)'.dependencies] async-std = { version = "1.12.0", features = ["unstable"] } -windows.workspace = true [dev-dependencies] tempfile.workspace = true diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 9ad14bddc4..33df4f7d15 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -9,7 +9,7 @@ use http_client::{HttpClient, Uri}; use semver::Version; use serde::Deserialize; use smol::io::BufReader; -use smol::{fs, lock::Mutex, process::Command}; +use smol::{fs, lock::Mutex}; use std::ffi::OsString; use std::io; use std::process::{Output, Stdio}; @@ -20,9 +20,6 @@ use std::{ }; use util::ResultExt; -#[cfg(windows)] -use smol::process::windows::CommandExt; - #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct NodeBinaryOptions { pub allow_path_lookup: bool, @@ -315,9 +312,7 @@ impl ManagedNodeRuntime { let node_binary = node_dir.join(Self::NODE_PATH); let npm_file = node_dir.join(Self::NPM_PATH); - let mut command = Command::new(&node_binary); - - command + let result = util::command::new_smol_command(&node_binary) .env_clear() .arg(npm_file) .arg("--version") @@ -326,12 +321,9 @@ impl ManagedNodeRuntime { .stderr(Stdio::null()) .args(["--cache".into(), node_dir.join("cache")]) .args(["--userconfig".into(), node_dir.join("blank_user_npmrc")]) - .args(["--globalconfig".into(), node_dir.join("blank_global_npmrc")]); - - #[cfg(windows)] - command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); - - let result = command.status().await; + .args(["--globalconfig".into(), node_dir.join("blank_global_npmrc")]) + .status() + .await; let valid = matches!(result, Ok(status) if status.success()); if !valid { @@ -412,7 +404,7 @@ impl NodeRuntimeTrait for ManagedNodeRuntime { return Err(anyhow!("missing npm file")); } - let mut command = Command::new(node_binary); + let mut command = util::command::new_smol_command(node_binary); command.env_clear(); command.env("PATH", env_path); command.arg(npm_file).arg(subcommand); @@ -473,7 +465,7 @@ pub struct SystemNodeRuntime { impl SystemNodeRuntime { const MIN_VERSION: semver::Version = Version::new(18, 0, 0); async fn new(node: PathBuf, npm: PathBuf) -> Result> { - let output = Command::new(&node) + let output = util::command::new_smol_command(&node) .arg("--version") .output() .await @@ -543,7 +535,7 @@ impl NodeRuntimeTrait for SystemNodeRuntime { subcommand: &str, args: &[&str], ) -> anyhow::Result { - let mut command = Command::new(self.npm.clone()); + let mut command = util::command::new_smol_command(self.npm.clone()); command .env_clear() .env("PATH", std::env::var_os("PATH").unwrap_or_default()) @@ -639,7 +631,11 @@ impl NodeRuntimeTrait for UnavailableNodeRuntime { } } -fn configure_npm_command(command: &mut Command, directory: Option<&Path>, proxy: Option<&Uri>) { +fn configure_npm_command( + command: &mut smol::process::Command, + directory: Option<&Path>, + proxy: Option<&Uri>, +) { if let Some(directory) = directory { command.current_dir(directory); command.args(["--prefix".into(), directory.to_path_buf()]); @@ -674,6 +670,5 @@ fn configure_npm_command(command: &mut Command, directory: Option<&Path>, proxy: { command.env("ComSpec", val); } - command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); } } diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs index 5c3de53ee1..a61f1da1c4 100644 --- a/crates/notifications/src/notification_store.rs +++ b/crates/notifications/src/notification_store.rs @@ -238,11 +238,8 @@ impl NotificationStore { ) -> Result<()> { this.update(&mut cx, |this, cx| { if let Some(notification) = envelope.payload.notification { - if let Some(rpc::Notification::ChannelMessageMention { - message_id, - sender_id: _, - channel_id: _, - }) = Notification::from_proto(¬ification) + if let Some(rpc::Notification::ChannelMessageMention { message_id, .. }) = + Notification::from_proto(¬ification) { let fetch_message_task = this.channel_store.update(cx, |this, cx| { this.fetch_channel_messages(vec![message_id], cx) diff --git a/crates/ollama/src/ollama.rs b/crates/ollama/src/ollama.rs index a133085020..5168da38be 100644 --- a/crates/ollama/src/ollama.rs +++ b/crates/ollama/src/ollama.rs @@ -81,9 +81,10 @@ fn get_max_tokens(name: &str) -> usize { "llama2" | "yi" | "vicuna" | "stablelm2" => 4096, "llama3" | "gemma2" | "gemma" | "codegemma" | "starcoder" | "aya" => 8192, "codellama" | "starcoder2" => 16384, - "mistral" | "codestral" | "mixstral" | "llava" | "qwen2" | "dolphin-mixtral" => 32768, + "mistral" | "codestral" | "mixstral" | "llava" | "qwen2" | "qwen2.5-coder" + | "dolphin-mixtral" => 32768, "llama3.1" | "phi3" | "phi3.5" | "command-r" | "deepseek-coder-v2" | "yi-coder" - | "llama3.2" | "qwen2.5-coder" => 128000, + | "llama3.2" => 128000, _ => DEFAULT_TOKENS, } .clamp(1, MAXIMUM_TOKENS) diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index a6d1903282..f378348782 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -2178,8 +2178,10 @@ impl OutlinePanel { .background_executor() .spawn(async move { let mut processed_external_buffers = HashSet::default(); - let mut new_worktree_entries = - HashMap::)>::default(); + let mut new_worktree_entries = HashMap::< + WorktreeId, + (worktree::Snapshot, HashMap), + >::default(); let mut worktree_excerpts = HashMap::< WorktreeId, HashMap)>, @@ -2213,7 +2215,7 @@ impl OutlinePanel { entry.path.as_ref(), ); - let mut entries_to_add = HashSet::default(); + let mut entries_to_add = HashMap::default(); worktree_excerpts .entry(worktree_id) .or_default() @@ -2238,7 +2240,9 @@ impl OutlinePanel { } } - let new_entry_added = entries_to_add.insert(current_entry); + let new_entry_added = entries_to_add + .insert(current_entry.id, current_entry) + .is_none(); if new_entry_added && traversal.back_to_parent() { if let Some(parent_entry) = traversal.entry() { current_entry = parent_entry.clone(); @@ -2249,7 +2253,7 @@ impl OutlinePanel { } new_worktree_entries .entry(worktree_id) - .or_insert_with(|| (worktree.clone(), HashSet::default())) + .or_insert_with(|| (worktree.clone(), HashMap::default())) .1 .extend(entries_to_add); } @@ -2276,7 +2280,7 @@ impl OutlinePanel { let worktree_entries = new_worktree_entries .into_iter() .map(|(worktree_id, (worktree_snapshot, entries))| { - let mut entries = entries.into_iter().collect::>(); + let mut entries = entries.into_values().collect::>(); // For a proper git status propagation, we have to keep the entries sorted lexicographically. entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref())); worktree_snapshot.propagate_git_statuses(&mut entries); @@ -3871,13 +3875,13 @@ impl OutlinePanel { .child({ let keystroke = match self.position(cx) { DockPosition::Left => { - cx.keystroke_text_for(&workspace::ToggleLeftDock) + cx.keystroke_text_for_action(&workspace::ToggleLeftDock) } DockPosition::Bottom => { - cx.keystroke_text_for(&workspace::ToggleBottomDock) + cx.keystroke_text_for_action(&workspace::ToggleBottomDock) } DockPosition::Right => { - cx.keystroke_text_for(&workspace::ToggleRightDock) + cx.keystroke_text_for_action(&workspace::ToggleRightDock) } }; Label::new(format!("Toggle this panel with {keystroke}")) diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 119c412b48..1cdb5af1af 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -425,6 +425,19 @@ impl Picker { self.cancel(&menu::Cancel, cx); } + pub fn refresh_placeholder(&mut self, cx: &mut WindowContext<'_>) { + match &self.head { + Head::Editor(view) => { + let placeholder = self.delegate.placeholder_text(cx); + view.update(cx, |this, cx| { + this.set_placeholder_text(placeholder, cx); + cx.notify(); + }); + } + Head::Empty(_) => {} + } + } + pub fn refresh(&mut self, cx: &mut ViewContext) { let query = self.query(cx); self.update_matches(query, cx); diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index 3a3b88cba1..92db62e6c6 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -225,6 +225,8 @@ impl Prettier { prettier_plugin_dir.join("dist").join("index.mjs"), prettier_plugin_dir.join("dist").join("index.js"), prettier_plugin_dir.join("dist").join("plugin.js"), + prettier_plugin_dir.join("src").join("plugin.js"), + prettier_plugin_dir.join("lib").join("index.js"), prettier_plugin_dir.join("index.mjs"), prettier_plugin_dir.join("index.js"), prettier_plugin_dir.join("plugin.js"), diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 18d603ca64..c19e85f5b9 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -75,9 +75,6 @@ url.workspace = true which.workspace = true fancy-regex.workspace = true -[target.'cfg(target_os = "windows")'.dependencies] -windows.workspace = true - [dev-dependencies] client = { workspace = true, features = ["test-support"] } collections = { workspace = true, features = ["test-support"] } diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 51d3f38588..837410f4d7 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -21,7 +21,7 @@ use language::{ deserialize_line_ending, deserialize_version, serialize_line_ending, serialize_version, split_operations, }, - Buffer, BufferEvent, Capability, File as _, Language, Operation, + Buffer, BufferEvent, Capability, DiskState, File as _, Language, Operation, }; use rpc::{proto, AnyProtoClient, ErrorExt as _, TypedEnvelope}; use smol::channel::Receiver; @@ -436,7 +436,10 @@ impl LocalBufferStore { let line_ending = buffer.line_ending(); let version = buffer.version(); let buffer_id = buffer.remote_id(); - if buffer.file().is_some_and(|file| !file.is_created()) { + if buffer + .file() + .is_some_and(|file| file.disk_state() == DiskState::New) + { has_changed_file = true; } @@ -446,7 +449,7 @@ impl LocalBufferStore { cx.spawn(move |this, mut cx| async move { let new_file = save.await?; - let mtime = new_file.mtime; + let mtime = new_file.disk_state().mtime(); this.update(&mut cx, |this, cx| { if let Some((downstream_client, project_id)) = this.downstream_client(cx) { if has_changed_file { @@ -660,37 +663,30 @@ impl LocalBufferStore { return None; } - let new_file = if let Some(entry) = old_file + let snapshot_entry = old_file .entry_id .and_then(|entry_id| snapshot.entry_for_id(entry_id)) - { + .or_else(|| snapshot.entry_for_path(old_file.path.as_ref())); + + let new_file = if let Some(entry) = snapshot_entry { File { + disk_state: match entry.mtime { + Some(mtime) => DiskState::Present { mtime }, + None => old_file.disk_state, + }, is_local: true, entry_id: Some(entry.id), - mtime: entry.mtime, path: entry.path.clone(), worktree: worktree.clone(), - is_deleted: false, - is_private: entry.is_private, - } - } else if let Some(entry) = snapshot.entry_for_path(old_file.path.as_ref()) { - File { - is_local: true, - entry_id: Some(entry.id), - mtime: entry.mtime, - path: entry.path.clone(), - worktree: worktree.clone(), - is_deleted: false, is_private: entry.is_private, } } else { File { + disk_state: DiskState::Deleted, is_local: true, entry_id: old_file.entry_id, path: old_file.path.clone(), - mtime: old_file.mtime, worktree: worktree.clone(), - is_deleted: true, is_private: old_file.is_private, } }; @@ -873,10 +869,9 @@ impl BufferStoreImpl for Model { Some(Arc::new(File { worktree, path, - mtime: None, + disk_state: DiskState::New, entry_id: None, is_local: true, - is_deleted: false, is_private: false, })), Capability::ReadWrite, @@ -913,30 +908,12 @@ impl BufferStoreImpl for Model { } fn create_buffer(&self, cx: &mut ModelContext) -> Task>> { - let handle = self.clone(); cx.spawn(|buffer_store, mut cx| async move { let buffer = cx.new_model(|cx| { Buffer::local("", cx).with_language(language::PLAIN_TEXT.clone(), cx) })?; buffer_store.update(&mut cx, |buffer_store, cx| { buffer_store.add_buffer(buffer.clone(), cx).log_err(); - let buffer_id = buffer.read(cx).remote_id(); - handle.update(cx, |this, cx| { - if let Some(file) = File::from_dyn(buffer.read(cx).file()) { - this.local_buffer_ids_by_path.insert( - ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }, - buffer_id, - ); - - if let Some(entry_id) = file.entry_id { - this.local_buffer_ids_by_entry_id - .insert(entry_id, buffer_id); - } - } - }); })?; Ok(buffer) }) diff --git a/crates/project/src/image_store.rs b/crates/project/src/image_store.rs index b3425e7fad..9f794d5248 100644 --- a/crates/project/src/image_store.rs +++ b/crates/project/src/image_store.rs @@ -9,7 +9,7 @@ use gpui::{ hash, prelude::*, AppContext, EventEmitter, Img, Model, ModelContext, Subscription, Task, WeakModel, }; -use language::File; +use language::{DiskState, File}; use rpc::{AnyProtoClient, ErrorExt as _}; use std::ffi::OsStr; use std::num::NonZeroU64; @@ -74,11 +74,12 @@ impl ImageItem { file_changed = true; } - if !new_file.is_deleted() { - let new_mtime = new_file.mtime(); - if new_mtime != old_file.mtime() { - file_changed = true; - cx.emit(ImageItemEvent::ReloadNeeded); + let old_state = old_file.disk_state(); + let new_state = new_file.disk_state(); + if old_state != new_state { + file_changed = true; + if matches!(new_state, DiskState::Present { .. }) { + cx.emit(ImageItemEvent::ReloadNeeded) } } @@ -503,37 +504,30 @@ impl LocalImageStore { return; } - let new_file = if let Some(entry) = old_file + let snapshot_entry = old_file .entry_id .and_then(|entry_id| snapshot.entry_for_id(entry_id)) - { + .or_else(|| snapshot.entry_for_path(old_file.path.as_ref())); + + let new_file = if let Some(entry) = snapshot_entry { worktree::File { + disk_state: match entry.mtime { + Some(mtime) => DiskState::Present { mtime }, + None => old_file.disk_state, + }, is_local: true, entry_id: Some(entry.id), - mtime: entry.mtime, path: entry.path.clone(), worktree: worktree.clone(), - is_deleted: false, - is_private: entry.is_private, - } - } else if let Some(entry) = snapshot.entry_for_path(old_file.path.as_ref()) { - worktree::File { - is_local: true, - entry_id: Some(entry.id), - mtime: entry.mtime, - path: entry.path.clone(), - worktree: worktree.clone(), - is_deleted: false, is_private: entry.is_private, } } else { worktree::File { + disk_state: DiskState::Deleted, is_local: true, entry_id: old_file.entry_id, path: old_file.path.clone(), - mtime: old_file.mtime, worktree: worktree.clone(), - is_deleted: true, is_private: old_file.is_private, } }; diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 57f8cea348..6de4902746 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2090,19 +2090,33 @@ impl LspCommand for GetCodeActions { server_id: LanguageServerId, _: AsyncAppContext, ) -> Result> { + let requested_kinds_set = if let Some(kinds) = self.kinds { + Some(kinds.into_iter().collect::>()) + } else { + None + }; + Ok(actions .unwrap_or_default() .into_iter() .filter_map(|entry| { - if let lsp::CodeActionOrCommand::CodeAction(lsp_action) = entry { - Some(CodeAction { - server_id, - range: self.range.clone(), - lsp_action, - }) - } else { - None + let lsp::CodeActionOrCommand::CodeAction(lsp_action) = entry else { + return None; + }; + + if let Some((requested_kinds, kind)) = + requested_kinds_set.as_ref().zip(lsp_action.kind.as_ref()) + { + if !requested_kinds.contains(kind) { + return None; + } } + + Some(CodeAction { + server_id, + range: self.range.clone(), + lsp_action, + }) }) .collect()) } diff --git a/crates/project/src/lsp_ext_command.rs b/crates/project/src/lsp_ext_command.rs index 9fa1dc5480..7890630e31 100644 --- a/crates/project/src/lsp_ext_command.rs +++ b/crates/project/src/lsp_ext_command.rs @@ -134,6 +134,132 @@ impl LspCommand for ExpandMacro { } } +pub enum LspOpenDocs {} + +impl lsp::request::Request for LspOpenDocs { + type Params = OpenDocsParams; + type Result = Option; + const METHOD: &'static str = "experimental/externalDocs"; +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct OpenDocsParams { + pub text_document: lsp::TextDocumentIdentifier, + pub position: lsp::Position, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct DocsUrls { + pub web: Option, + pub local: Option, +} + +impl DocsUrls { + pub fn is_empty(&self) -> bool { + self.web.is_none() && self.local.is_none() + } +} + +#[derive(Debug)] +pub struct OpenDocs { + pub position: PointUtf16, +} + +#[async_trait(?Send)] +impl LspCommand for OpenDocs { + type Response = DocsUrls; + type LspRequest = LspOpenDocs; + type ProtoRequest = proto::LspExtOpenDocs; + + fn to_lsp( + &self, + path: &Path, + _: &Buffer, + _: &Arc, + _: &AppContext, + ) -> OpenDocsParams { + OpenDocsParams { + text_document: lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), + }, + position: point_to_lsp(self.position), + } + } + + async fn response_from_lsp( + self, + message: Option, + _: Model, + _: Model, + _: LanguageServerId, + _: AsyncAppContext, + ) -> anyhow::Result { + Ok(message + .map(|message| DocsUrls { + web: message.web, + local: message.local, + }) + .unwrap_or_default()) + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtOpenDocs { + proto::LspExtOpenDocs { + project_id, + buffer_id: buffer.remote_id().into(), + position: Some(language::proto::serialize_anchor( + &buffer.anchor_before(self.position), + )), + } + } + + async fn from_proto( + message: Self::ProtoRequest, + _: Model, + buffer: Model, + mut cx: AsyncAppContext, + ) -> anyhow::Result { + let position = message + .position + .and_then(deserialize_anchor) + .context("invalid position")?; + Ok(Self { + position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + }) + } + + fn response_to_proto( + response: DocsUrls, + _: &mut LspStore, + _: PeerId, + _: &clock::Global, + _: &mut AppContext, + ) -> proto::LspExtOpenDocsResponse { + proto::LspExtOpenDocsResponse { + web: response.web, + local: response.local, + } + } + + async fn response_from_proto( + self, + message: proto::LspExtOpenDocsResponse, + _: Model, + _: Model, + _: AsyncAppContext, + ) -> anyhow::Result { + Ok(DocsUrls { + web: message.web, + local: message.local, + }) + } + + fn buffer_id_from_proto(message: &proto::LspExtOpenDocs) -> Result { + BufferId::new(message.buffer_id) + } +} + pub enum LspSwitchSourceHeader {} impl lsp::request::Request for LspSwitchSourceHeader { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 733548787d..0346bbce71 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -613,12 +613,7 @@ impl LocalLspStore { Some(worktree_path) })?; - let mut child = smol::process::Command::new(command); - #[cfg(target_os = "windows")] - { - use smol::process::windows::CommandExt; - child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); - } + let mut child = util::command::new_smol_command(command); if let Some(buffer_env) = buffer.env.as_ref() { child.envs(buffer_env); @@ -2032,6 +2027,7 @@ impl LspStore { &mut self, buffer_handle: &Model, range: Range, + kinds: Option>, cx: &mut ModelContext, ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { @@ -2045,7 +2041,7 @@ impl LspStore { request: Some(proto::multi_lsp_query::Request::GetCodeActions( GetCodeActions { range: range.clone(), - kinds: None, + kinds: kinds.clone(), } .to_proto(project_id, buffer_handle.read(cx)), )), @@ -2071,7 +2067,7 @@ impl LspStore { .map(|code_actions_response| { GetCodeActions { range: range.clone(), - kinds: None, + kinds: kinds.clone(), } .response_from_proto( code_actions_response, @@ -2096,7 +2092,7 @@ impl LspStore { Some(range.start), GetCodeActions { range: range.clone(), - kinds: None, + kinds: kinds.clone(), }, cx, ); @@ -5539,10 +5535,16 @@ impl LspStore { .unwrap_or_default(), allow_binary_download, }; + let toolchains = self.toolchain_store(cx); cx.spawn(|_, mut cx| async move { let binary_result = adapter .clone() - .get_language_server_command(delegate.clone(), lsp_binary_options, &mut cx) + .get_language_server_command( + delegate.clone(), + toolchains, + lsp_binary_options, + &mut cx, + ) .await; delegate.update_status(adapter.name.clone(), LanguageServerBinaryStatus::None); @@ -7799,6 +7801,7 @@ impl LspAdapter for SshLspAdapter { async fn check_if_user_installed( &self, _: &dyn LspAdapterDelegate, + _: Arc, _: &AsyncAppContext, ) -> Option { Some(self.binary.clone()) @@ -7947,7 +7950,7 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate { }; let env = self.shell_env().await; - let output = smol::process::Command::new(&npm) + let output = util::command::new_smol_command(&npm) .args(["root", "-g"]) .envs(env) .current_dir(local_package_directory) @@ -7981,7 +7984,7 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate { async fn try_exec(&self, command: LanguageServerBinary) -> Result<()> { let working_dir = self.worktree_root_path(); - let output = smol::process::Command::new(&command.path) + let output = util::command::new_smol_command(&command.path) .args(command.arguments) .envs(command.env.clone().unwrap_or_default()) .current_dir(working_dir) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e6a0fb7601..d6aca6d40e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -61,8 +61,8 @@ use language::{ Transaction, Unclipped, }; use lsp::{ - CompletionContext, CompletionItemKind, DocumentHighlightKind, LanguageServer, LanguageServerId, - LanguageServerName, MessageActionItem, + CodeActionKind, CompletionContext, CompletionItemKind, DocumentHighlightKind, LanguageServer, + LanguageServerId, LanguageServerName, MessageActionItem, }; use lsp_command::*; use node_runtime::NodeRuntime; @@ -1446,7 +1446,7 @@ impl Project { let fs = Arc::new(RealFs::default()); let languages = LanguageRegistry::test(cx.background_executor().clone()); - let clock = Arc::new(FakeSystemClock::default()); + let clock = Arc::new(FakeSystemClock::new()); let http_client = http_client::FakeHttpClient::with_404_response(); let client = cx .update(|cx| client::Client::new(clock, http_client.clone(), cx)) @@ -1492,7 +1492,7 @@ impl Project { use gpui::Context; let languages = LanguageRegistry::test(cx.executor()); - let clock = Arc::new(FakeSystemClock::default()); + let clock = Arc::new(FakeSystemClock::new()); let http_client = http_client::FakeHttpClient::with_404_response(); let client = cx.update(|cx| client::Client::new(clock, http_client.clone(), cx)); let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx)); @@ -2824,6 +2824,19 @@ impl Project { Task::ready(None) } } + + pub async fn toolchain_term( + languages: Arc, + language_name: LanguageName, + ) -> Option { + languages + .language_for_name(&language_name.0) + .await + .ok()? + .toolchain_lister() + .map(|lister| lister.term()) + } + pub fn activate_toolchain( &self, worktree_id: WorktreeId, @@ -3190,12 +3203,13 @@ impl Project { &mut self, buffer_handle: &Model, range: Range, + kinds: Option>, cx: &mut ModelContext, ) -> Task>> { let buffer = buffer_handle.read(cx); let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end); self.lsp_store.update(cx, |lsp_store, cx| { - lsp_store.code_actions(buffer_handle, range, cx) + lsp_store.code_actions(buffer_handle, range, kinds, cx) }) } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 7bdb8493d0..cc59dbfbc9 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -5,12 +5,12 @@ use gpui::{AppContext, SemanticVersion, UpdateGlobal}; use http_client::Url; use language::{ language_settings::{language_settings, AllLanguageSettings, LanguageSettingsContent}, - tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticSet, FakeLspAdapter, + tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticSet, DiskState, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint, }; use lsp::{DiagnosticSeverity, NumberOrString}; use parking_lot::Mutex; -use pretty_assertions::assert_eq; +use pretty_assertions::{assert_eq, assert_matches}; use serde_json::json; #[cfg(not(windows))] use std::os; @@ -2793,7 +2793,9 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { let fake_server = fake_language_servers.next().await.unwrap(); // Language server returns code actions that contain commands, and not edits. - let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx)); + let actions = project.update(cx, |project, cx| { + project.code_actions(&buffer, 0..0, None, cx) + }); fake_server .handle_request::(|_, _| async move { Ok(Some(vec![ @@ -3240,10 +3242,22 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) { Path::new("b/c/file5") ); - assert!(!buffer2.read(cx).file().unwrap().is_deleted()); - assert!(!buffer3.read(cx).file().unwrap().is_deleted()); - assert!(!buffer4.read(cx).file().unwrap().is_deleted()); - assert!(buffer5.read(cx).file().unwrap().is_deleted()); + assert_matches!( + buffer2.read(cx).file().unwrap().disk_state(), + DiskState::Present { .. } + ); + assert_matches!( + buffer3.read(cx).file().unwrap().disk_state(), + DiskState::Present { .. } + ); + assert_matches!( + buffer4.read(cx).file().unwrap().disk_state(), + DiskState::Present { .. } + ); + assert_eq!( + buffer5.read(cx).file().unwrap().disk_state(), + DiskState::Deleted + ); }); // Update the remote worktree. Check that it becomes consistent with the @@ -3417,7 +3431,11 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { ] ); events.lock().clear(); - buffer.did_save(buffer.version(), buffer.file().unwrap().mtime(), cx); + buffer.did_save( + buffer.version(), + buffer.file().unwrap().disk_state().mtime(), + cx, + ); }); // after saving, the buffer is not dirty, and emits a saved event. @@ -4946,6 +4964,84 @@ async fn test_hovers_with_empty_parts(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_code_actions_only_kinds(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/dir", + json!({ + "a.ts": "a", + }), + ) + .await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(typescript_lang()); + let mut fake_language_servers = language_registry.register_fake_lsp( + "TypeScript", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)), + ..lsp::ServerCapabilities::default() + }, + ..FakeLspAdapter::default() + }, + ); + + let buffer = project + .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx)) + .await + .unwrap(); + cx.executor().run_until_parked(); + + let fake_server = fake_language_servers + .next() + .await + .expect("failed to get the language server"); + + let mut request_handled = fake_server.handle_request::( + move |_, _| async move { + Ok(Some(vec![ + lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { + title: "organize imports".to_string(), + kind: Some(CodeActionKind::SOURCE_ORGANIZE_IMPORTS), + ..lsp::CodeAction::default() + }), + lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { + title: "fix code".to_string(), + kind: Some(CodeActionKind::SOURCE_FIX_ALL), + ..lsp::CodeAction::default() + }), + ])) + }, + ); + + let code_actions_task = project.update(cx, |project, cx| { + project.code_actions( + &buffer, + 0..buffer.read(cx).len(), + Some(vec![CodeActionKind::SOURCE_ORGANIZE_IMPORTS]), + cx, + ) + }); + + let () = request_handled + .next() + .await + .expect("The code action request should have been triggered"); + + let code_actions = code_actions_task.await.unwrap(); + assert_eq!(code_actions.len(), 1); + assert_eq!( + code_actions[0].lsp_action.kind, + Some(CodeActionKind::SOURCE_ORGANIZE_IMPORTS) + ); +} + #[gpui::test] async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) { init_test(cx); @@ -5077,7 +5173,7 @@ async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) { } let code_actions_task = project.update(cx, |project, cx| { - project.code_actions(&buffer, 0..buffer.read(cx).len(), cx) + project.code_actions(&buffer, 0..buffer.read(cx).len(), None, cx) }); // cx.run_until_parked(); diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index f740556b15..2df0d3360a 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -204,6 +204,8 @@ impl Project { command_label: spawn_task.command_label, hide: spawn_task.hide, status: TaskStatus::Running, + show_summary: spawn_task.show_summary, + show_command: spawn_task.show_command, completion_rx, }); diff --git a/crates/project/src/toolchain_store.rs b/crates/project/src/toolchain_store.rs index c601ff8f12..4d4c32d745 100644 --- a/crates/project/src/toolchain_store.rs +++ b/crates/project/src/toolchain_store.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{str::FromStr, sync::Arc}; use anyhow::{bail, Result}; @@ -119,6 +119,7 @@ impl ToolchainStore { let toolchain = Toolchain { name: toolchain.name.into(), path: toolchain.path.into(), + as_json: serde_json::Value::from_str(&toolchain.raw_json)?, language_name, }; let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); @@ -144,6 +145,7 @@ impl ToolchainStore { toolchain: toolchain.map(|toolchain| proto::Toolchain { name: toolchain.name.into(), path: toolchain.path.into(), + raw_json: toolchain.as_json.to_string(), }), }) } @@ -182,6 +184,7 @@ impl ToolchainStore { .map(|toolchain| proto::Toolchain { name: toolchain.name.to_string(), path: toolchain.path.to_string(), + raw_json: toolchain.as_json.to_string(), }) .collect::>() } else { @@ -352,6 +355,7 @@ impl RemoteToolchainStore { toolchain: Some(proto::Toolchain { name: toolchain.name.into(), path: toolchain.path.into(), + raw_json: toolchain.as_json.to_string(), }), }) .await @@ -383,10 +387,13 @@ impl RemoteToolchainStore { let toolchains = response .toolchains .into_iter() - .map(|toolchain| Toolchain { - language_name: language_name.clone(), - name: toolchain.name.into(), - path: toolchain.path.into(), + .filter_map(|toolchain| { + Some(Toolchain { + language_name: language_name.clone(), + name: toolchain.name.into(), + path: toolchain.path.into(), + as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?, + }) }) .collect(); let groups = response @@ -421,10 +428,13 @@ impl RemoteToolchainStore { .await .log_err()?; - response.toolchain.map(|toolchain| Toolchain { - language_name: language_name.clone(), - name: toolchain.name.into(), - path: toolchain.path.into(), + response.toolchain.and_then(|toolchain| { + Some(Toolchain { + language_name: language_name.clone(), + name: toolchain.name.into(), + path: toolchain.path.into(), + as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?, + }) }) }) } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index a0a921ad0c..5ee5867367 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -40,6 +40,7 @@ use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use std::{ cell::OnceCell, + cmp, collections::HashSet, ffi::OsStr, ops::Range, @@ -53,7 +54,7 @@ use ui::{ IndentGuideColors, IndentGuideLayout, KeyBinding, Label, ListItem, Scrollbar, ScrollbarState, Tooltip, }; -use util::{maybe, ResultExt, TryFutureExt}; +use util::{maybe, paths::compare_paths, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, notifications::{DetachAndPromptErr, NotifyTaskExt}, @@ -101,6 +102,7 @@ pub struct ProjectPanel { // We keep track of the mouse down state on entries so we don't flash the UI // in case a user clicks to open a file. mouse_down: bool, + hovered_entries: HashSet, } #[derive(Clone, Debug)] @@ -139,6 +141,7 @@ struct EntryDetails { is_marked: bool, is_editing: bool, is_processing: bool, + is_hovered: bool, is_cut: bool, filename_text_color: Color, diagnostic_severity: Option, @@ -256,7 +259,7 @@ fn get_item_color(cx: &ViewContext) -> ItemColors { ItemColors { default: colors.surface_background, - hover: colors.element_active, + hover: colors.ghost_element_hover, drag_over: colors.drop_target_background, marked_active: colors.ghost_element_selected, } @@ -380,6 +383,7 @@ impl ProjectPanel { diagnostics: Default::default(), scroll_handle, mouse_down: false, + hovered_entries: Default::default(), }; this.update_visible_entries(None, cx); @@ -547,7 +551,7 @@ impl ProjectPanel { .entry((project_path.worktree_id, path_buffer.clone())) .and_modify(|strongest_diagnostic_severity| { *strongest_diagnostic_severity = - std::cmp::min(*strongest_diagnostic_severity, diagnostic_severity); + cmp::min(*strongest_diagnostic_severity, diagnostic_severity); }) .or_insert(diagnostic_severity); } @@ -1197,15 +1201,15 @@ impl ProjectPanel { fn remove(&mut self, trash: bool, skip_prompt: bool, cx: &mut ViewContext<'_, ProjectPanel>) { maybe!({ - if self.marked_entries.is_empty() && self.selection.is_none() { + let items_to_delete = self.disjoint_entries_for_removal(cx); + if items_to_delete.is_empty() { return None; } let project = self.project.read(cx); - let items_to_delete = self.marked_entries(); let mut dirty_buffers = 0; let file_paths = items_to_delete - .into_iter() + .iter() .filter_map(|selection| { let project_path = project.path_for_entry(selection.entry_id, cx)?; dirty_buffers += @@ -1274,28 +1278,120 @@ impl ProjectPanel { } else { None }; - - cx.spawn(|this, mut cx| async move { + let next_selection = self.find_next_selection_after_deletion(items_to_delete, cx); + cx.spawn(|panel, mut cx| async move { if let Some(answer) = answer { if answer.await != Ok(0) { - return Result::<(), anyhow::Error>::Ok(()); + return anyhow::Ok(()); } } for (entry_id, _) in file_paths { - this.update(&mut cx, |this, cx| { - this.project - .update(cx, |project, cx| project.delete_entry(entry_id, trash, cx)) - .ok_or_else(|| anyhow!("no such entry")) - })?? - .await?; + panel + .update(&mut cx, |panel, cx| { + panel + .project + .update(cx, |project, cx| project.delete_entry(entry_id, trash, cx)) + .context("no such entry") + })?? + .await?; } - Result::<(), anyhow::Error>::Ok(()) + panel.update(&mut cx, |panel, cx| { + if let Some(next_selection) = next_selection { + panel.selection = Some(next_selection); + panel.autoscroll(cx); + } else { + panel.select_last(&SelectLast {}, cx); + } + })?; + Ok(()) }) .detach_and_log_err(cx); Some(()) }); } + fn find_next_selection_after_deletion( + &self, + sanitized_entries: BTreeSet, + cx: &mut ViewContext, + ) -> Option { + if sanitized_entries.is_empty() { + return None; + } + + let project = self.project.read(cx); + let (worktree_id, worktree) = sanitized_entries + .iter() + .map(|entry| entry.worktree_id) + .filter_map(|id| project.worktree_for_id(id, cx).map(|w| (id, w.read(cx)))) + .max_by(|(_, a), (_, b)| a.root_name().cmp(b.root_name()))?; + + let marked_entries_in_worktree = sanitized_entries + .iter() + .filter(|e| e.worktree_id == worktree_id) + .collect::>(); + let latest_entry = marked_entries_in_worktree + .iter() + .max_by(|a, b| { + match ( + worktree.entry_for_id(a.entry_id), + worktree.entry_for_id(b.entry_id), + ) { + (Some(a), Some(b)) => { + compare_paths((&a.path, a.is_file()), (&b.path, b.is_file())) + } + _ => cmp::Ordering::Equal, + } + }) + .and_then(|e| worktree.entry_for_id(e.entry_id))?; + + let parent_path = latest_entry.path.parent()?; + let parent_entry = worktree.entry_for_path(parent_path)?; + + // Remove all siblings that are being deleted except the last marked entry + let mut siblings: Vec = worktree + .snapshot() + .child_entries(parent_path) + .filter(|sibling| { + sibling.id == latest_entry.id + || !marked_entries_in_worktree.contains(&&SelectedEntry { + worktree_id, + entry_id: sibling.id, + }) + }) + .cloned() + .collect(); + + project::sort_worktree_entries(&mut siblings); + let sibling_entry_index = siblings + .iter() + .position(|sibling| sibling.id == latest_entry.id)?; + + if let Some(next_sibling) = sibling_entry_index + .checked_add(1) + .and_then(|i| siblings.get(i)) + { + return Some(SelectedEntry { + worktree_id, + entry_id: next_sibling.id, + }); + } + if let Some(prev_sibling) = sibling_entry_index + .checked_sub(1) + .and_then(|i| siblings.get(i)) + { + return Some(SelectedEntry { + worktree_id, + entry_id: prev_sibling.id, + }); + } + // No neighbour sibling found, fall back to parent + Some(SelectedEntry { + worktree_id, + entry_id: parent_entry.id, + }) + } + fn unfold_directory(&mut self, _: &UnfoldDirectory, cx: &mut ViewContext) { if let Some((worktree, entry)) = self.selected_entry(cx) { self.unfolded_dir_ids.insert(entry.id); @@ -1848,6 +1944,54 @@ impl ProjectPanel { None } + fn disjoint_entries_for_removal(&self, cx: &AppContext) -> BTreeSet { + let marked_entries = self.marked_entries(); + let mut sanitized_entries = BTreeSet::new(); + if marked_entries.is_empty() { + return sanitized_entries; + } + + let project = self.project.read(cx); + let marked_entries_by_worktree: HashMap> = marked_entries + .into_iter() + .filter(|entry| !project.entry_is_worktree_root(entry.entry_id, cx)) + .fold(HashMap::default(), |mut map, entry| { + map.entry(entry.worktree_id).or_default().push(entry); + map + }); + + for (worktree_id, marked_entries) in marked_entries_by_worktree { + if let Some(worktree) = project.worktree_for_id(worktree_id, cx) { + let worktree = worktree.read(cx); + let marked_dir_paths = marked_entries + .iter() + .filter_map(|entry| { + worktree.entry_for_id(entry.entry_id).and_then(|entry| { + if entry.is_dir() { + Some(entry.path.as_ref()) + } else { + None + } + }) + }) + .collect::>(); + + sanitized_entries.extend(marked_entries.into_iter().filter(|entry| { + let Some(entry_info) = worktree.entry_for_id(entry.entry_id) else { + return false; + }; + let entry_path = entry_info.path.as_ref(); + let inside_marked_dir = marked_dir_paths.iter().any(|&marked_dir_path| { + entry_path != marked_dir_path && entry_path.starts_with(marked_dir_path) + }); + !inside_marked_dir + })); + } + } + + sanitized_entries + } + // Returns list of entries that should be affected by an operation. // When currently selected entry is not marked, it's treated as the only marked entry. fn marked_entries(&self) -> BTreeSet { @@ -2049,6 +2193,7 @@ impl ProjectPanel { is_ignored: entry.is_ignored, is_external: false, is_private: false, + is_always_included: entry.is_always_included, git_status: entry.git_status, canonical_path: entry.canonical_path.clone(), char_bag: entry.char_bag, @@ -2480,6 +2625,7 @@ impl ProjectPanel { is_expanded, is_selected: self.selection == Some(selection), is_marked, + is_hovered: self.hovered_entries.contains(&entry.id), is_editing: false, is_processing: false, is_cut: self @@ -2609,6 +2755,7 @@ impl ProjectPanel { let is_active = self .selection .map_or(false, |selection| selection.entry_id == entry_id); + let is_hovered = details.is_hovered; let width = self.size(cx); let file_name = details.filename.clone(); @@ -2641,6 +2788,14 @@ impl ProjectPanel { marked_selections: selections, }; + let (bg_color, border_color) = match (is_hovered, is_marked || is_active, self.mouse_down) { + (true, _, true) => (item_colors.marked_active, item_colors.hover), + (true, false, false) => (item_colors.hover, item_colors.hover), + (true, true, false) => (item_colors.hover, item_colors.marked_active), + (false, true, _) => (item_colors.marked_active, item_colors.marked_active), + _ => (item_colors.default, item_colors.default), + }; + div() .id(entry_id.to_proto() as usize) .when(is_local, |div| { @@ -2718,6 +2873,14 @@ impl ProjectPanel { cx.propagate(); }), ) + .on_hover(cx.listener(move |this, hover, cx| { + if *hover { + this.hovered_entries.insert(entry_id); + } else { + this.hovered_entries.remove(&entry_id); + } + cx.notify(); + })) .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| { if event.down.button == MouseButton::Right || event.down.first_mouse || show_editor { @@ -2778,11 +2941,13 @@ impl ProjectPanel { } })) .cursor_pointer() + .bg(bg_color) + .border_color(border_color) .child( ListItem::new(entry_id.to_proto() as usize) .indent_level(depth) .indent_step_size(px(settings.indent_size)) - .selected(is_marked || is_active) + .selectable(false) .when_some(canonical_path, |this, path| { this.end_slot::( div() @@ -2822,11 +2987,7 @@ impl ProjectPanel { } else { IconDecorationKind::Dot }, - if is_marked || is_active { - item_colors.marked_active - } else { - item_colors.default - }, + bg_color, cx, ) .color(decoration_color.color(cx)) @@ -2939,19 +3100,6 @@ impl ProjectPanel { .border_1() .border_r_2() .rounded_none() - .hover(|style| { - if is_active { - style - } else { - style.bg(item_colors.hover).border_color(item_colors.hover) - } - }) - .when(is_marked || is_active, |this| { - this.when(is_marked, |this| { - this.bg(item_colors.marked_active) - .border_color(item_colors.marked_active) - }) - }) .when( !self.mouse_down && is_active && self.focus_handle.contains_focused(cx), |this| this.border_color(Color::Selected.color(cx)), @@ -5089,14 +5237,13 @@ mod tests { &[ "v src", " v test", - " second.rs", + " second.rs <== selected", " third.rs" ], "Project panel should have no deleted file, no other file is selected in it" ); ensure_no_open_items_and_panes(&workspace, cx); - select_path(&panel, "src/test/second.rs", cx); panel.update(cx, |panel, cx| panel.open(&Open, cx)); cx.executor().run_until_parked(); assert_eq!( @@ -5130,7 +5277,7 @@ mod tests { submit_deletion_skipping_prompt(&panel, cx); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), - &["v src", " v test", " third.rs"], + &["v src", " v test", " third.rs <== selected"], "Project panel should have no deleted file, with one last file remaining" ); ensure_no_open_items_and_panes(&workspace, cx); @@ -5639,7 +5786,11 @@ mod tests { submit_deletion(&panel, cx); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), - &["v project_root", " v dir_1", " v nested_dir",] + &[ + "v project_root", + " v dir_1", + " v nested_dir <== selected", + ] ); } #[gpui::test] @@ -6336,6 +6487,598 @@ mod tests { ); } + #[gpui::test] + async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root", + json!({ + "dir1": { + "subdir1": {}, + "file1.txt": "", + "file2.txt": "", + }, + "dir2": { + "subdir2": {}, + "file3.txt": "", + "file4.txt": "", + }, + "file5.txt": "", + "file6.txt": "", + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + + toggle_expand_dir(&panel, "root/dir1", cx); + toggle_expand_dir(&panel, "root/dir2", cx); + + // Test Case 1: Delete middle file in directory + select_path(&panel, "root/dir1/file1.txt", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..15, cx), + &[ + "v root", + " v dir1", + " > subdir1", + " file1.txt <== selected", + " file2.txt", + " v dir2", + " > subdir2", + " file3.txt", + " file4.txt", + " file5.txt", + " file6.txt", + ], + "Initial state before deleting middle file" + ); + + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..15, cx), + &[ + "v root", + " v dir1", + " > subdir1", + " file2.txt <== selected", + " v dir2", + " > subdir2", + " file3.txt", + " file4.txt", + " file5.txt", + " file6.txt", + ], + "Should select next file after deleting middle file" + ); + + // Test Case 2: Delete last file in directory + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..15, cx), + &[ + "v root", + " v dir1", + " > subdir1 <== selected", + " v dir2", + " > subdir2", + " file3.txt", + " file4.txt", + " file5.txt", + " file6.txt", + ], + "Should select next directory when last file is deleted" + ); + + // Test Case 3: Delete root level file + select_path(&panel, "root/file6.txt", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..15, cx), + &[ + "v root", + " v dir1", + " > subdir1", + " v dir2", + " > subdir2", + " file3.txt", + " file4.txt", + " file5.txt", + " file6.txt <== selected", + ], + "Initial state before deleting root level file" + ); + + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..15, cx), + &[ + "v root", + " v dir1", + " > subdir1", + " v dir2", + " > subdir2", + " file3.txt", + " file4.txt", + " file5.txt <== selected", + ], + "Should select prev entry at root level" + ); + } + + #[gpui::test] + async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root", + json!({ + "dir1": { + "subdir1": { + "a.txt": "", + "b.txt": "" + }, + "file1.txt": "", + }, + "dir2": { + "subdir2": { + "c.txt": "", + "d.txt": "" + }, + "file2.txt": "", + }, + "file3.txt": "", + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + + toggle_expand_dir(&panel, "root/dir1", cx); + toggle_expand_dir(&panel, "root/dir1/subdir1", cx); + toggle_expand_dir(&panel, "root/dir2", cx); + toggle_expand_dir(&panel, "root/dir2/subdir2", cx); + + // Test Case 1: Select and delete nested directory with parent + cx.simulate_modifiers_change(gpui::Modifiers { + control: true, + ..Default::default() + }); + select_path_with_mark(&panel, "root/dir1/subdir1", cx); + select_path_with_mark(&panel, "root/dir1", cx); + + assert_eq!( + visible_entries_as_strings(&panel, 0..15, cx), + &[ + "v root", + " v dir1 <== selected <== marked", + " v subdir1 <== marked", + " a.txt", + " b.txt", + " file1.txt", + " v dir2", + " v subdir2", + " c.txt", + " d.txt", + " file2.txt", + " file3.txt", + ], + "Initial state before deleting nested directory with parent" + ); + + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..15, cx), + &[ + "v root", + " v dir2 <== selected", + " v subdir2", + " c.txt", + " d.txt", + " file2.txt", + " file3.txt", + ], + "Should select next directory after deleting directory with parent" + ); + + // Test Case 2: Select mixed files and directories across levels + select_path_with_mark(&panel, "root/dir2/subdir2/c.txt", cx); + select_path_with_mark(&panel, "root/dir2/file2.txt", cx); + select_path_with_mark(&panel, "root/file3.txt", cx); + + assert_eq!( + visible_entries_as_strings(&panel, 0..15, cx), + &[ + "v root", + " v dir2", + " v subdir2", + " c.txt <== marked", + " d.txt", + " file2.txt <== marked", + " file3.txt <== selected <== marked", + ], + "Initial state before deleting" + ); + + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..15, cx), + &[ + "v root", + " v dir2 <== selected", + " v subdir2", + " d.txt", + ], + "Should select sibling directory" + ); + } + + #[gpui::test] + async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root", + json!({ + "dir1": { + "subdir1": { + "a.txt": "", + "b.txt": "" + }, + "file1.txt": "", + }, + "dir2": { + "subdir2": { + "c.txt": "", + "d.txt": "" + }, + "file2.txt": "", + }, + "file3.txt": "", + "file4.txt": "", + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + + toggle_expand_dir(&panel, "root/dir1", cx); + toggle_expand_dir(&panel, "root/dir1/subdir1", cx); + toggle_expand_dir(&panel, "root/dir2", cx); + toggle_expand_dir(&panel, "root/dir2/subdir2", cx); + + // Test Case 1: Select all root files and directories + cx.simulate_modifiers_change(gpui::Modifiers { + control: true, + ..Default::default() + }); + select_path_with_mark(&panel, "root/dir1", cx); + select_path_with_mark(&panel, "root/dir2", cx); + select_path_with_mark(&panel, "root/file3.txt", cx); + select_path_with_mark(&panel, "root/file4.txt", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v root", + " v dir1 <== marked", + " v subdir1", + " a.txt", + " b.txt", + " file1.txt", + " v dir2 <== marked", + " v subdir2", + " c.txt", + " d.txt", + " file2.txt", + " file3.txt <== marked", + " file4.txt <== selected <== marked", + ], + "State before deleting all contents" + ); + + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &["v root <== selected"], + "Only empty root directory should remain after deleting all contents" + ); + } + + #[gpui::test] + async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root", + json!({ + "dir1": { + "subdir1": { + "file_a.txt": "content a", + "file_b.txt": "content b", + }, + "subdir2": { + "file_c.txt": "content c", + }, + "file1.txt": "content 1", + }, + "dir2": { + "file2.txt": "content 2", + }, + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + + toggle_expand_dir(&panel, "root/dir1", cx); + toggle_expand_dir(&panel, "root/dir1/subdir1", cx); + toggle_expand_dir(&panel, "root/dir2", cx); + cx.simulate_modifiers_change(gpui::Modifiers { + control: true, + ..Default::default() + }); + + // Test Case 1: Select parent directory, subdirectory, and a file inside the subdirectory + select_path_with_mark(&panel, "root/dir1", cx); + select_path_with_mark(&panel, "root/dir1/subdir1", cx); + select_path_with_mark(&panel, "root/dir1/subdir1/file_a.txt", cx); + + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v root", + " v dir1 <== marked", + " v subdir1 <== marked", + " file_a.txt <== selected <== marked", + " file_b.txt", + " > subdir2", + " file1.txt", + " v dir2", + " file2.txt", + ], + "State with parent dir, subdir, and file selected" + ); + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &["v root", " v dir2 <== selected", " file2.txt",], + "Only dir2 should remain after deletion" + ); + } + + #[gpui::test] + async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + // First worktree + fs.insert_tree( + "/root1", + json!({ + "dir1": { + "file1.txt": "content 1", + "file2.txt": "content 2", + }, + "dir2": { + "file3.txt": "content 3", + }, + }), + ) + .await; + + // Second worktree + fs.insert_tree( + "/root2", + json!({ + "dir3": { + "file4.txt": "content 4", + "file5.txt": "content 5", + }, + "file6.txt": "content 6", + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + + // Expand all directories for testing + toggle_expand_dir(&panel, "root1/dir1", cx); + toggle_expand_dir(&panel, "root1/dir2", cx); + toggle_expand_dir(&panel, "root2/dir3", cx); + + // Test Case 1: Delete files across different worktrees + cx.simulate_modifiers_change(gpui::Modifiers { + control: true, + ..Default::default() + }); + select_path_with_mark(&panel, "root1/dir1/file1.txt", cx); + select_path_with_mark(&panel, "root2/dir3/file4.txt", cx); + + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v root1", + " v dir1", + " file1.txt <== marked", + " file2.txt", + " v dir2", + " file3.txt", + "v root2", + " v dir3", + " file4.txt <== selected <== marked", + " file5.txt", + " file6.txt", + ], + "Initial state with files selected from different worktrees" + ); + + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v root1", + " v dir1", + " file2.txt", + " v dir2", + " file3.txt", + "v root2", + " v dir3", + " file5.txt <== selected", + " file6.txt", + ], + "Should select next file in the last worktree after deletion" + ); + + // Test Case 2: Delete directories from different worktrees + select_path_with_mark(&panel, "root1/dir1", cx); + select_path_with_mark(&panel, "root2/dir3", cx); + + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v root1", + " v dir1 <== marked", + " file2.txt", + " v dir2", + " file3.txt", + "v root2", + " v dir3 <== selected <== marked", + " file5.txt", + " file6.txt", + ], + "State with directories marked from different worktrees" + ); + + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v root1", + " v dir2", + " file3.txt", + "v root2", + " file6.txt <== selected", + ], + "Should select remaining file in last worktree after directory deletion" + ); + + // Test Case 4: Delete all remaining files except roots + select_path_with_mark(&panel, "root1/dir2/file3.txt", cx); + select_path_with_mark(&panel, "root2/file6.txt", cx); + + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v root1", + " v dir2", + " file3.txt <== marked", + "v root2", + " file6.txt <== selected <== marked", + ], + "State with all remaining files marked" + ); + + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &["v root1", " v dir2", "v root2 <== selected"], + "Second parent root should be selected after deleting" + ); + } + + #[gpui::test] + async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root_b", + json!({ + "dir1": { + "file1.txt": "content 1", + "file2.txt": "content 2", + }, + }), + ) + .await; + + fs.insert_tree( + "/root_c", + json!({ + "dir2": {}, + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root_b".as_ref(), "/root_c".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + + toggle_expand_dir(&panel, "root_b/dir1", cx); + toggle_expand_dir(&panel, "root_c/dir2", cx); + + cx.simulate_modifiers_change(gpui::Modifiers { + control: true, + ..Default::default() + }); + select_path_with_mark(&panel, "root_b/dir1/file1.txt", cx); + select_path_with_mark(&panel, "root_b/dir1/file2.txt", cx); + + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v root_b", + " v dir1", + " file1.txt <== marked", + " file2.txt <== selected <== marked", + "v root_c", + " v dir2", + ], + "Initial state with files marked in root_b" + ); + + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v root_b", + " v dir1 <== selected", + "v root_c", + " v dir2", + ], + "After deletion in root_b as it's last deletion, selection should be in root_b" + ); + + select_path_with_mark(&panel, "root_c/dir2", cx); + + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &["v root_b", " v dir1", "v root_c <== selected",], + "After deleting from root_c, it should remain in root_c" + ); + } + fn toggle_expand_dir( panel: &View, path: impl AsRef, @@ -6373,6 +7116,32 @@ mod tests { }); } + fn select_path_with_mark( + panel: &View, + path: impl AsRef, + cx: &mut VisualTestContext, + ) { + let path = path.as_ref(); + panel.update(cx, |panel, cx| { + for worktree in panel.project.read(cx).worktrees(cx).collect::>() { + let worktree = worktree.read(cx); + if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { + let entry_id = worktree.entry_for_path(relative_path).unwrap().id; + let entry = crate::SelectedEntry { + worktree_id: worktree.id(), + entry_id, + }; + if !panel.marked_entries.contains(&entry) { + panel.marked_entries.insert(entry); + } + panel.selection = Some(entry); + return; + } + } + panic!("no worktree for path {:?}", path); + }); + } + fn find_project_entry( panel: &View, path: impl AsRef, diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 10e9c41a0d..aa83917abc 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -276,6 +276,7 @@ message Envelope { LanguageServerPromptRequest language_server_prompt_request = 268; LanguageServerPromptResponse language_server_prompt_response = 269; + GitBranches git_branches = 270; GitBranchesResponse git_branches_response = 271; @@ -295,7 +296,14 @@ message Envelope { CancelLanguageServerWork cancel_language_server_work = 282; - SynchronizeBreakpoints Synchronize_breakpoints = 283; // current max + LspExtOpenDocs lsp_ext_open_docs = 283; + LspExtOpenDocsResponse lsp_ext_open_docs_response = 284; + + SyncExtensions sync_extensions = 285; + SyncExtensionsResponse sync_extensions_response = 286; + InstallExtension install_extension = 287; + + SynchronizeBreakpoints Synchronize_breakpoints = 288; // current max } reserved 87 to 88; @@ -2028,6 +2036,17 @@ message LspExtExpandMacroResponse { string expansion = 2; } +message LspExtOpenDocs { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; +} + +message LspExtOpenDocsResponse { + optional string web = 1; + optional string local = 2; +} + message LspExtSwitchSourceHeader { uint64 project_id = 1; uint64 buffer_id = 2; @@ -2478,6 +2497,7 @@ message ListToolchains { message Toolchain { string name = 1; string path = 2; + string raw_json = 3; } message ToolchainGroup { @@ -2553,3 +2573,23 @@ message CancelLanguageServerWork { optional string token = 2; } } + +message Extension { + string id = 1; + string version = 2; + bool dev = 3; +} + +message SyncExtensions { + repeated Extension extensions = 1; +} + +message SyncExtensionsResponse { + string tmp_dir = 1; + repeated Extension missing_extensions = 2; +} + +message InstallExtension { + Extension extension = 1; + string tmp_dir = 2; +} diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index e83bdf8b04..71c6f34057 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -314,6 +314,8 @@ messages!( (UsersResponse, Foreground), (LspExtExpandMacro, Background), (LspExtExpandMacroResponse, Background), + (LspExtOpenDocs, Background), + (LspExtOpenDocsResponse, Background), (SetRoomParticipantRole, Foreground), (BlameBuffer, Foreground), (BlameBufferResponse, Foreground), @@ -366,7 +368,10 @@ messages!( (GetPanicFiles, Background), (GetPanicFilesResponse, Background), (CancelLanguageServerWork, Foreground), - (SynchronizeBreakpoints, Foreground), + (SyncExtensions, Background), + (SyncExtensionsResponse, Background), + (InstallExtension, Background), + (SynchronizeBreakpoints, Background), ); request_messages!( @@ -465,6 +470,7 @@ request_messages!( (UpdateProject, Ack), (UpdateWorktree, Ack), (LspExtExpandMacro, LspExtExpandMacroResponse), + (LspExtOpenDocs, LspExtOpenDocsResponse), (SetRoomParticipantRole, Ack), (BlameBuffer, BlameBufferResponse), (RejoinRemoteProjects, RejoinRemoteProjectsResponse), @@ -489,6 +495,8 @@ request_messages!( (GetPathMetadata, GetPathMetadataResponse), (GetPanicFiles, GetPanicFilesResponse), (CancelLanguageServerWork, Ack), + (SyncExtensions, SyncExtensionsResponse), + (InstallExtension, Ack), ); entity_messages!( @@ -553,6 +561,7 @@ entity_messages!( UpdateWorktree, UpdateWorktreeSettings, LspExtExpandMacro, + LspExtOpenDocs, AdvertiseContexts, OpenContext, CreateContext, diff --git a/crates/quick_action_bar/Cargo.toml b/crates/quick_action_bar/Cargo.toml deleted file mode 100644 index b3228820f6..0000000000 --- a/crates/quick_action_bar/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "quick_action_bar" -version = "0.1.0" -edition = "2021" -publish = false -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/quick_action_bar.rs" -doctest = false - -[dependencies] -assistant.workspace = true -editor.workspace = true -gpui.workspace = true -markdown_preview.workspace = true -repl.workspace = true -search.workspace = true -settings.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -zed_actions.workspace = true -picker.workspace = true - -[dev-dependencies] -editor = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index a12a1da56b..e7b0c07f67 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -17,6 +17,7 @@ anyhow.workspace = true auto_update.workspace = true release_channel.workspace = true editor.workspace = true +extension_host.workspace = true file_finder.workspace = true futures.workspace = true fuzzy.workspace = true @@ -40,6 +41,7 @@ ui.workspace = true util.workspace = true workspace.workspace = true paths.workspace = true +zed_actions.workspace = true [dev-dependencies] dap = { workspace = true } diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index bf822496f7..bc781f63cd 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -16,7 +16,6 @@ use picker::{ Picker, PickerDelegate, }; pub use remote_servers::RemoteServerProjects; -use serde::Deserialize; use settings::Settings; pub use ssh_connections::SshSettings; use std::{ @@ -29,19 +28,7 @@ use workspace::{ CloseIntent, ModalView, OpenOptions, SerializedWorkspaceLocation, Workspace, WorkspaceId, WORKSPACE_DB, }; - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct OpenRecent { - #[serde(default = "default_create_new_window")] - pub create_new_window: bool, -} - -fn default_create_new_window() -> bool { - false -} - -gpui::impl_actions!(projects, [OpenRecent]); -gpui::actions!(projects, [OpenRemote]); +use zed_actions::{OpenRecent, OpenRemote}; pub fn init(cx: &mut AppContext) { SshSettings::register(cx); @@ -185,13 +172,13 @@ impl PickerDelegate for RecentProjectsDelegate { fn placeholder_text(&self, cx: &mut WindowContext) -> Arc { let (create_window, reuse_window) = if self.create_new_window { ( - cx.keystroke_text_for(&menu::Confirm), - cx.keystroke_text_for(&menu::SecondaryConfirm), + cx.keystroke_text_for_action(&menu::Confirm), + cx.keystroke_text_for_action(&menu::SecondaryConfirm), ) } else { ( - cx.keystroke_text_for(&menu::SecondaryConfirm), - cx.keystroke_text_for(&menu::Confirm), + cx.keystroke_text_for_action(&menu::SecondaryConfirm), + cx.keystroke_text_for_action(&menu::Confirm), ) }; Arc::from(format!( diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index e70b68d374..a9aeacadd8 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -4,6 +4,7 @@ use std::{path::PathBuf, sync::Arc, time::Duration}; use anyhow::{anyhow, Result}; use auto_update::AutoUpdater; use editor::Editor; +use extension_host::ExtensionStore; use futures::channel::oneshot; use gpui::{ percentage, Animation, AnimationExt, AnyWindowHandle, AsyncAppContext, DismissEvent, @@ -630,6 +631,15 @@ pub async fn open_ssh_project( } } + window + .update(cx, |workspace, cx| { + if let Some(client) = workspace.project().read(cx).ssh_client().clone() { + ExtensionStore::global(cx) + .update(cx, |store, cx| store.register_ssh_client(client, cx)); + } + }) + .ok(); + break; } diff --git a/crates/remote/Cargo.toml b/crates/remote/Cargo.toml index 8a199c56f6..69d1d97c59 100644 --- a/crates/remote/Cargo.toml +++ b/crates/remote/Cargo.toml @@ -38,6 +38,7 @@ tempfile.workspace = true thiserror.workspace = true util.workspace = true release_channel.workspace = true +which.workspace = true [dev-dependencies] gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index c607f0a0ec..546135c30b 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -255,7 +255,7 @@ impl SshSocket { // and passes -l as an argument to sh, not to ls. // You need to do it like this: $ ssh host "sh -c 'ls -l /tmp'" fn ssh_command(&self, program: &str, args: &[&str]) -> process::Command { - let mut command = process::Command::new("ssh"); + let mut command = util::command::new_smol_command("ssh"); let to_run = iter::once(&program) .chain(args.iter()) .map(|token| { @@ -1074,7 +1074,7 @@ impl SshRemoteClient { c.connections.insert( opts.clone(), ConnectionPoolEntry::Connecting( - cx.foreground_executor() + cx.background_executor() .spawn({ let connection = connection.clone(); async move { Ok(connection.clone()) } @@ -1224,7 +1224,7 @@ trait RemoteConnection: Send + Sync { struct SshRemoteConnection { socket: SshSocket, - master_process: Mutex>, + master_process: Mutex>, remote_binary_path: Option, _temp_dir: TempDir, } @@ -1258,7 +1258,7 @@ impl RemoteConnection for SshRemoteConnection { dest_path: PathBuf, cx: &AppContext, ) -> Task> { - let mut command = process::Command::new("scp"); + let mut command = util::command::new_smol_command("scp"); let output = self .socket .ssh_options(&mut command) @@ -1269,6 +1269,7 @@ impl RemoteConnection for SshRemoteConnection { .map(|port| vec!["-P".to_string(), port.to_string()]) .unwrap_or_default(), ) + .arg("-C") .arg("-r") .arg(&src_path) .arg(format!( @@ -1428,9 +1429,21 @@ impl SshRemoteConnection { } }); + anyhow::ensure!( + which::which("nc").is_ok(), + "Cannot find nc, which is required to connect over ssh." + ); + // Create an askpass script that communicates back to this process. let askpass_script = format!( - "{shebang}\n{print_args} | nc -U {askpass_socket} 2> /dev/null \n", + "{shebang}\n{print_args} | {nc} -U {askpass_socket} 2> /dev/null \n", + // on macOS `brew install netcat` provides the GNU netcat implementation + // which does not support -U. + nc = if cfg!(target_os = "macos") { + "/usr/bin/nc" + } else { + "nc" + }, askpass_socket = askpass_socket.display(), print_args = "printf '%s\\0' \"$@\"", shebang = "#!/bin/sh", @@ -1556,6 +1569,7 @@ impl SshRemoteConnection { // exclude armv5,6,7 as they are 32-bit. let arch = if arch.starts_with("armv8") || arch.starts_with("armv9") + || arch.starts_with("arm64") || arch.starts_with("aarch64") { "aarch64" @@ -1897,7 +1911,7 @@ impl SshRemoteConnection { async fn upload_file(&self, src_path: &Path, dest_path: &Path) -> Result<()> { log::debug!("uploading file {:?} to {:?}", src_path, dest_path); - let mut command = process::Command::new("scp"); + let mut command = util::command::new_smol_command("scp"); let output = self .socket .ssh_options(&mut command) diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index 73e52895df..82853217dc 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -29,6 +29,8 @@ chrono.workspace = true clap.workspace = true client.workspace = true env_logger.workspace = true +extension.workspace = true +extension_host.workspace = true fs.workspace = true futures.workspace = true git.workspace = true @@ -36,6 +38,7 @@ git_hosting_providers.workspace = true gpui.workspace = true http_client.workspace = true language.workspace = true +language_extension.workspace = true languages.workspace = true log.workspace = true lsp.workspace = true diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 7ddfde92b2..f876f50661 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -1,4 +1,6 @@ use anyhow::{anyhow, Result}; +use extension::ExtensionHostProxy; +use extension_host::headless_host::HeadlessExtensionStore; use fs::Fs; use gpui::{AppContext, AsyncAppContext, Context as _, Model, ModelContext, PromptLevel}; use http_client::HttpClient; @@ -38,6 +40,7 @@ pub struct HeadlessProject { pub settings_observer: Model, pub next_entry_id: Arc, pub languages: Arc, + pub extensions: Model, } pub struct HeadlessAppState { @@ -46,6 +49,7 @@ pub struct HeadlessAppState { pub http_client: Arc, pub node_runtime: NodeRuntime, pub languages: Arc, + pub extension_host_proxy: Arc, } impl HeadlessProject { @@ -62,9 +66,11 @@ impl HeadlessProject { http_client, node_runtime, languages, + extension_host_proxy: proxy, }: HeadlessAppState, cx: &mut ModelContext, ) -> Self { + language_extension::init(proxy.clone(), languages.clone()); languages::init(languages.clone(), node_runtime.clone(), cx); let worktree_store = cx.new_model(|cx| { @@ -163,6 +169,15 @@ impl HeadlessProject { ) .detach(); + let extensions = HeadlessExtensionStore::new( + fs.clone(), + http_client.clone(), + paths::remote_extensions_dir().to_path_buf(), + proxy, + node_runtime, + cx, + ); + let client: AnyProtoClient = session.clone().into(); session.subscribe_to_entity(SSH_PROJECT_ID, &worktree_store); @@ -190,6 +205,15 @@ impl HeadlessProject { client.add_model_request_handler(BufferStore::handle_update_buffer); client.add_model_message_handler(BufferStore::handle_close_buffer); + client.add_request_handler( + extensions.clone().downgrade(), + HeadlessExtensionStore::handle_sync_extensions, + ); + client.add_request_handler( + extensions.clone().downgrade(), + HeadlessExtensionStore::handle_install_extension, + ); + BufferStore::init(&client); WorktreeStore::init(&client); SettingsObserver::init(&client); @@ -208,6 +232,7 @@ impl HeadlessProject { task_store, next_entry_id: Default::default(), languages, + extensions, } } diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index e3914c7ae1..bdb862c5af 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -1,6 +1,7 @@ use crate::headless_project::HeadlessProject; use client::{Client, UserStore}; use clock::FakeSystemClock; +use extension::ExtensionHostProxy; use fs::{FakeFs, Fs}; use gpui::{Context, Model, SemanticVersion, TestAppContext}; use http_client::{BlockedHttpClient, FakeHttpClient}; @@ -1234,6 +1235,7 @@ pub async fn init_test( let http_client = Arc::new(BlockedHttpClient); let node_runtime = NodeRuntime::unavailable(); let languages = Arc::new(LanguageRegistry::new(cx.executor())); + let proxy = Arc::new(ExtensionHostProxy::new()); server_cx.update(HeadlessProject::init); let headless = server_cx.new_model(|cx| { client::init_settings(cx); @@ -1245,6 +1247,7 @@ pub async fn init_test( http_client, node_runtime, languages, + extension_host_proxy: proxy, }, cx, ) @@ -1277,7 +1280,7 @@ fn build_project(ssh: Model, cx: &mut TestAppContext) -> Model< let client = cx.update(|cx| { Client::new( - Arc::new(FakeSystemClock::default()), + Arc::new(FakeSystemClock::new()), FakeHttpClient::with_404_response(), cx, ) diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 467fd452f8..18378ec8e9 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -3,6 +3,7 @@ use crate::HeadlessProject; use anyhow::{anyhow, Context, Result}; use chrono::Utc; use client::{telemetry, ProxySettings}; +use extension::ExtensionHostProxy; use fs::{Fs, RealFs}; use futures::channel::mpsc; use futures::{select, select_biased, AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt, SinkExt}; @@ -434,6 +435,9 @@ pub fn execute_run( GitHostingProviderRegistry::set_global(git_hosting_provider_registry, cx); git_hosting_providers::init(cx); + extension::init(cx); + let extension_host_proxy = ExtensionHostProxy::global(cx); + let project = cx.new_model(|cx| { let fs = Arc::new(RealFs::new(Default::default(), None)); let node_settings_rx = initialize_settings(session.clone(), fs.clone(), cx); @@ -466,6 +470,7 @@ pub fn execute_run( http_client, node_runtime, languages, + extension_host_proxy, }, cx, ) diff --git a/crates/repl/Cargo.toml b/crates/repl/Cargo.toml index b170def71f..293a58e762 100644 --- a/crates/repl/Cargo.toml +++ b/crates/repl/Cargo.toml @@ -22,9 +22,13 @@ collections.workspace = true command_palette_hooks.workspace = true editor.workspace = true feature_flags.workspace = true +file_icons.workspace = true futures.workspace = true gpui.workspace = true +http_client.workspace = true image.workspace = true +jupyter-websocket-client.workspace = true +jupyter-protocol.workspace = true language.workspace = true log.workspace = true markdown_preview.workspace = true @@ -47,9 +51,6 @@ uuid.workspace = true workspace.workspace = true picker.workspace = true -[target.'cfg(target_os = "windows")'.dependencies] -windows.workspace = true - [dev-dependencies] editor = { workspace = true, features = ["test-support"] } env_logger.workspace = true diff --git a/crates/repl/src/components/kernel_options.rs b/crates/repl/src/components/kernel_options.rs index fc0213e54e..8fd9b412ea 100644 --- a/crates/repl/src/components/kernel_options.rs +++ b/crates/repl/src/components/kernel_options.rs @@ -34,6 +34,16 @@ pub struct KernelPickerDelegate { on_select: OnSelect, } +// Helper function to truncate long paths +fn truncate_path(path: &SharedString, max_length: usize) -> SharedString { + if path.len() <= max_length { + path.to_string().into() + } else { + let truncated = path.chars().rev().take(max_length - 3).collect::(); + format!("...{}", truncated.chars().rev().collect::()).into() + } +} + impl KernelSelector { pub fn new(on_select: OnSelect, worktree_id: WorktreeId, trigger: T) -> Self { KernelSelector { @@ -116,11 +126,25 @@ impl PickerDelegate for KernelPickerDelegate { &self, ix: usize, selected: bool, - _cx: &mut ViewContext>, + cx: &mut ViewContext>, ) -> Option { let kernelspec = self.filtered_kernels.get(ix)?; - let is_selected = self.selected_kernelspec.as_ref() == Some(kernelspec); + let icon = kernelspec.icon(cx); + + let (name, kernel_type, path_or_url) = match kernelspec { + KernelSpecification::Jupyter(_) => (kernelspec.name(), "Jupyter", None), + KernelSpecification::PythonEnv(_) => ( + kernelspec.name(), + "Python Env", + Some(truncate_path(&kernelspec.path(), 42)), + ), + KernelSpecification::Remote(_) => ( + kernelspec.name(), + "Remote", + Some(truncate_path(&kernelspec.path(), 42)), + ), + }; Some( ListItem::new(ix) @@ -128,25 +152,46 @@ impl PickerDelegate for KernelPickerDelegate { .spacing(ListItemSpacing::Sparse) .selected(selected) .child( - v_flex() - .min_w(px(600.)) + h_flex() .w_full() - .gap_0p5() + .gap_3() + .child(icon.color(Color::Default).size(IconSize::Medium)) .child( - h_flex() - .w_full() - .gap_1() - .child(Label::new(kernelspec.name()).weight(FontWeight::MEDIUM)) + v_flex() + .flex_grow() + .gap_0p5() .child( - Label::new(kernelspec.language()) - .size(LabelSize::Small) - .color(Color::Muted), + h_flex() + .justify_between() + .child( + div().w_48().text_ellipsis().child( + Label::new(name) + .weight(FontWeight::MEDIUM) + .size(LabelSize::Default), + ), + ) + .when_some(path_or_url.clone(), |flex, path| { + flex.text_ellipsis().child( + Label::new(path) + .size(LabelSize::Small) + .color(Color::Muted), + ) + }), + ) + .child( + h_flex() + .gap_1() + .child( + Label::new(kernelspec.language()) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child( + Label::new(kernel_type) + .size(LabelSize::Small) + .color(Color::Muted), + ), ), - ) - .child( - Label::new(kernelspec.path()) - .size(LabelSize::XSmall) - .color(Color::Muted), ), ) .when(is_selected, |item| { @@ -199,7 +244,9 @@ impl RenderOnce for KernelSelector { }; let picker_view = cx.new_view(|cx| { - let picker = Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into())); + let picker = Picker::uniform_list(delegate, cx) + .width(rems(30.)) + .max_height(Some(rems(20.).into())); picker }); diff --git a/crates/repl/src/kernels/mod.rs b/crates/repl/src/kernels/mod.rs new file mode 100644 index 0000000000..e829b1946c --- /dev/null +++ b/crates/repl/src/kernels/mod.rs @@ -0,0 +1,240 @@ +mod native_kernel; +use std::{fmt::Debug, future::Future, path::PathBuf}; + +use futures::{ + channel::mpsc::{self, Receiver}, + future::Shared, + stream, +}; +use gpui::{AppContext, Model, Task, WindowContext}; +use language::LanguageName; +pub use native_kernel::*; + +mod remote_kernels; +use project::{Project, WorktreeId}; +pub use remote_kernels::*; + +use anyhow::Result; +use jupyter_protocol::JupyterKernelspec; +use runtimelib::{ExecutionState, JupyterMessage, KernelInfoReply}; +use ui::{Icon, IconName, SharedString}; + +pub type JupyterMessageChannel = stream::SelectAll>; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum KernelSpecification { + Remote(RemoteKernelSpecification), + Jupyter(LocalKernelSpecification), + PythonEnv(LocalKernelSpecification), +} + +impl KernelSpecification { + pub fn name(&self) -> SharedString { + match self { + Self::Jupyter(spec) => spec.name.clone().into(), + Self::PythonEnv(spec) => spec.name.clone().into(), + Self::Remote(spec) => spec.name.clone().into(), + } + } + + pub fn type_name(&self) -> SharedString { + match self { + Self::Jupyter(_) => "Jupyter".into(), + Self::PythonEnv(_) => "Python Environment".into(), + Self::Remote(_) => "Remote".into(), + } + } + + pub fn path(&self) -> SharedString { + SharedString::from(match self { + Self::Jupyter(spec) => spec.path.to_string_lossy().to_string(), + Self::PythonEnv(spec) => spec.path.to_string_lossy().to_string(), + Self::Remote(spec) => spec.url.to_string(), + }) + } + + pub fn language(&self) -> SharedString { + SharedString::from(match self { + Self::Jupyter(spec) => spec.kernelspec.language.clone(), + Self::PythonEnv(spec) => spec.kernelspec.language.clone(), + Self::Remote(spec) => spec.kernelspec.language.clone(), + }) + } + + pub fn icon(&self, cx: &AppContext) -> Icon { + let lang_name = match self { + Self::Jupyter(spec) => spec.kernelspec.language.clone(), + Self::PythonEnv(spec) => spec.kernelspec.language.clone(), + Self::Remote(spec) => spec.kernelspec.language.clone(), + }; + + file_icons::FileIcons::get(cx) + .get_type_icon(&lang_name.to_lowercase()) + .map(Icon::from_path) + .unwrap_or(Icon::new(IconName::ReplNeutral)) + } +} + +pub fn python_env_kernel_specifications( + project: &Model, + worktree_id: WorktreeId, + cx: &mut AppContext, +) -> impl Future>> { + let python_language = LanguageName::new("Python"); + let toolchains = project + .read(cx) + .available_toolchains(worktree_id, python_language, cx); + let background_executor = cx.background_executor().clone(); + + async move { + let toolchains = if let Some(toolchains) = toolchains.await { + toolchains + } else { + return Ok(Vec::new()); + }; + + let kernelspecs = toolchains.toolchains.into_iter().map(|toolchain| { + background_executor.spawn(async move { + let python_path = toolchain.path.to_string(); + + // Check if ipykernel is installed + let ipykernel_check = util::command::new_smol_command(&python_path) + .args(&["-c", "import ipykernel"]) + .output() + .await; + + if ipykernel_check.is_ok() && ipykernel_check.unwrap().status.success() { + // Create a default kernelspec for this environment + let default_kernelspec = JupyterKernelspec { + argv: vec![ + python_path.clone(), + "-m".to_string(), + "ipykernel_launcher".to_string(), + "-f".to_string(), + "{connection_file}".to_string(), + ], + display_name: toolchain.name.to_string(), + language: "python".to_string(), + interrupt_mode: None, + metadata: None, + env: None, + }; + + Some(KernelSpecification::PythonEnv(LocalKernelSpecification { + name: toolchain.name.to_string(), + path: PathBuf::from(&python_path), + kernelspec: default_kernelspec, + })) + } else { + None + } + }) + }); + + let kernel_specs = futures::future::join_all(kernelspecs) + .await + .into_iter() + .flatten() + .collect(); + + anyhow::Ok(kernel_specs) + } +} + +pub trait RunningKernel: Send + Debug { + fn request_tx(&self) -> mpsc::Sender; + fn working_directory(&self) -> &PathBuf; + fn execution_state(&self) -> &ExecutionState; + fn set_execution_state(&mut self, state: ExecutionState); + fn kernel_info(&self) -> Option<&KernelInfoReply>; + fn set_kernel_info(&mut self, info: KernelInfoReply); + fn force_shutdown(&mut self, cx: &mut WindowContext) -> Task>; +} + +#[derive(Debug, Clone)] +pub enum KernelStatus { + Idle, + Busy, + Starting, + Error, + ShuttingDown, + Shutdown, + Restarting, +} + +impl KernelStatus { + pub fn is_connected(&self) -> bool { + match self { + KernelStatus::Idle | KernelStatus::Busy => true, + _ => false, + } + } +} + +impl ToString for KernelStatus { + fn to_string(&self) -> String { + match self { + KernelStatus::Idle => "Idle".to_string(), + KernelStatus::Busy => "Busy".to_string(), + KernelStatus::Starting => "Starting".to_string(), + KernelStatus::Error => "Error".to_string(), + KernelStatus::ShuttingDown => "Shutting Down".to_string(), + KernelStatus::Shutdown => "Shutdown".to_string(), + KernelStatus::Restarting => "Restarting".to_string(), + } + } +} + +#[derive(Debug)] +pub enum Kernel { + RunningKernel(Box), + StartingKernel(Shared>), + ErroredLaunch(String), + ShuttingDown, + Shutdown, + Restarting, +} + +impl From<&Kernel> for KernelStatus { + fn from(kernel: &Kernel) -> Self { + match kernel { + Kernel::RunningKernel(kernel) => match kernel.execution_state() { + ExecutionState::Idle => KernelStatus::Idle, + ExecutionState::Busy => KernelStatus::Busy, + }, + Kernel::StartingKernel(_) => KernelStatus::Starting, + Kernel::ErroredLaunch(_) => KernelStatus::Error, + Kernel::ShuttingDown => KernelStatus::ShuttingDown, + Kernel::Shutdown => KernelStatus::Shutdown, + Kernel::Restarting => KernelStatus::Restarting, + } + } +} + +impl Kernel { + pub fn status(&self) -> KernelStatus { + self.into() + } + + pub fn set_execution_state(&mut self, status: &ExecutionState) { + if let Kernel::RunningKernel(running_kernel) = self { + running_kernel.set_execution_state(status.clone()); + } + } + + pub fn set_kernel_info(&mut self, kernel_info: &KernelInfoReply) { + if let Kernel::RunningKernel(running_kernel) = self { + running_kernel.set_kernel_info(kernel_info.clone()); + } + } + + pub fn is_shutting_down(&self) -> bool { + match self { + Kernel::Restarting | Kernel::ShuttingDown => true, + Kernel::RunningKernel(_) + | Kernel::StartingKernel(_) + | Kernel::ErroredLaunch(_) + | Kernel::Shutdown => false, + } + } +} diff --git a/crates/repl/src/kernels.rs b/crates/repl/src/kernels/native_kernel.rs similarity index 55% rename from crates/repl/src/kernels.rs rename to crates/repl/src/kernels/native_kernel.rs index 8ad8a05648..974a721ac5 100644 --- a/crates/repl/src/kernels.rs +++ b/crates/repl/src/kernels/native_kernel.rs @@ -1,69 +1,27 @@ use anyhow::{Context as _, Result}; use futures::{ - channel::mpsc::{self, Receiver}, - future::Shared, - stream::{self, SelectAll, StreamExt}, - SinkExt as _, -}; -use gpui::{AppContext, EntityId, Model, Task}; -use language::LanguageName; -use project::{Fs, Project, WorktreeId}; -use runtimelib::{ - dirs, ConnectionInfo, ExecutionState, JupyterKernelspec, JupyterMessage, JupyterMessageContent, - KernelInfoReply, + channel::mpsc::{self}, + io::BufReader, + stream::{SelectAll, StreamExt}, + AsyncBufReadExt as _, SinkExt as _, }; +use gpui::{EntityId, Task, View, WindowContext}; +use jupyter_protocol::{JupyterKernelspec, JupyterMessage, JupyterMessageContent, KernelInfoReply}; +use project::Fs; +use runtimelib::{dirs, ConnectionInfo, ExecutionState}; use smol::{net::TcpListener, process::Command}; use std::{ env, fmt::Debug, - future::Future, net::{IpAddr, Ipv4Addr, SocketAddr}, path::PathBuf, sync::Arc, }; -use ui::SharedString; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum KernelSpecification { - Remote(RemoteKernelSpecification), - Jupyter(LocalKernelSpecification), - PythonEnv(LocalKernelSpecification), -} +use crate::Session; -impl KernelSpecification { - pub fn name(&self) -> SharedString { - match self { - Self::Jupyter(spec) => spec.name.clone().into(), - Self::PythonEnv(spec) => spec.name.clone().into(), - Self::Remote(spec) => spec.name.clone().into(), - } - } - - pub fn type_name(&self) -> SharedString { - match self { - Self::Jupyter(_) => "Jupyter".into(), - Self::PythonEnv(_) => "Python Environment".into(), - Self::Remote(_) => "Remote".into(), - } - } - - pub fn path(&self) -> SharedString { - SharedString::from(match self { - Self::Jupyter(spec) => spec.path.to_string_lossy().to_string(), - Self::PythonEnv(spec) => spec.path.to_string_lossy().to_string(), - Self::Remote(spec) => spec.url.to_string(), - }) - } - - pub fn language(&self) -> SharedString { - SharedString::from(match self { - Self::Jupyter(spec) => spec.kernelspec.language.clone(), - Self::PythonEnv(spec) => spec.kernelspec.language.clone(), - Self::Remote(spec) => spec.kernelspec.language.clone(), - }) - } -} +use super::RunningKernel; #[derive(Debug, Clone)] pub struct LocalKernelSpecification { @@ -80,22 +38,6 @@ impl PartialEq for LocalKernelSpecification { impl Eq for LocalKernelSpecification {} -#[derive(Debug, Clone)] -pub struct RemoteKernelSpecification { - pub name: String, - pub url: String, - pub token: String, - pub kernelspec: JupyterKernelspec, -} - -impl PartialEq for RemoteKernelSpecification { - fn eq(&self, other: &Self) -> bool { - self.name == other.name && self.url == other.url - } -} - -impl Eq for RemoteKernelSpecification {} - impl LocalKernelSpecification { #[must_use] fn command(&self, connection_path: &PathBuf) -> Result { @@ -109,7 +51,7 @@ impl LocalKernelSpecification { self.name ); - let mut cmd = Command::new(&argv[0]); + let mut cmd = util::command::new_smol_command(&argv[0]); for arg in &argv[1..] { if arg == "{connection_file}" { @@ -123,12 +65,6 @@ impl LocalKernelSpecification { cmd.envs(env); } - #[cfg(windows)] - { - use smol::process::windows::CommandExt; - cmd.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); - } - Ok(cmd) } } @@ -147,110 +83,20 @@ async fn peek_ports(ip: IpAddr) -> Result<[u16; 5]> { Ok(ports) } -#[derive(Debug, Clone)] -pub enum KernelStatus { - Idle, - Busy, - Starting, - Error, - ShuttingDown, - Shutdown, - Restarting, -} - -impl KernelStatus { - pub fn is_connected(&self) -> bool { - match self { - KernelStatus::Idle | KernelStatus::Busy => true, - _ => false, - } - } -} - -impl ToString for KernelStatus { - fn to_string(&self) -> String { - match self { - KernelStatus::Idle => "Idle".to_string(), - KernelStatus::Busy => "Busy".to_string(), - KernelStatus::Starting => "Starting".to_string(), - KernelStatus::Error => "Error".to_string(), - KernelStatus::ShuttingDown => "Shutting Down".to_string(), - KernelStatus::Shutdown => "Shutdown".to_string(), - KernelStatus::Restarting => "Restarting".to_string(), - } - } -} - -impl From<&Kernel> for KernelStatus { - fn from(kernel: &Kernel) -> Self { - match kernel { - Kernel::RunningKernel(kernel) => match kernel.execution_state { - ExecutionState::Idle => KernelStatus::Idle, - ExecutionState::Busy => KernelStatus::Busy, - }, - Kernel::StartingKernel(_) => KernelStatus::Starting, - Kernel::ErroredLaunch(_) => KernelStatus::Error, - Kernel::ShuttingDown => KernelStatus::ShuttingDown, - Kernel::Shutdown => KernelStatus::Shutdown, - Kernel::Restarting => KernelStatus::Restarting, - } - } -} - -#[derive(Debug)] -pub enum Kernel { - RunningKernel(RunningKernel), - StartingKernel(Shared>), - ErroredLaunch(String), - ShuttingDown, - Shutdown, - Restarting, -} - -impl Kernel { - pub fn status(&self) -> KernelStatus { - self.into() - } - - pub fn set_execution_state(&mut self, status: &ExecutionState) { - if let Kernel::RunningKernel(running_kernel) = self { - running_kernel.execution_state = status.clone(); - } - } - - pub fn set_kernel_info(&mut self, kernel_info: &KernelInfoReply) { - if let Kernel::RunningKernel(running_kernel) = self { - running_kernel.kernel_info = Some(kernel_info.clone()); - } - } - - pub fn is_shutting_down(&self) -> bool { - match self { - Kernel::Restarting | Kernel::ShuttingDown => true, - Kernel::RunningKernel(_) - | Kernel::StartingKernel(_) - | Kernel::ErroredLaunch(_) - | Kernel::Shutdown => false, - } - } -} - -pub struct RunningKernel { +pub struct NativeRunningKernel { pub process: smol::process::Child, _shell_task: Task>, - _iopub_task: Task>, _control_task: Task>, _routing_task: Task>, connection_path: PathBuf, + _process_status_task: Option>, pub working_directory: PathBuf, pub request_tx: mpsc::Sender, pub execution_state: ExecutionState, pub kernel_info: Option, } -type JupyterMessageChannel = stream::SelectAll>; - -impl Debug for RunningKernel { +impl Debug for NativeRunningKernel { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("RunningKernel") .field("process", &self.process) @@ -258,25 +104,16 @@ impl Debug for RunningKernel { } } -impl RunningKernel { +impl NativeRunningKernel { pub fn new( - kernel_specification: KernelSpecification, + kernel_specification: LocalKernelSpecification, entity_id: EntityId, working_directory: PathBuf, fs: Arc, - cx: &mut AppContext, - ) -> Task> { - let kernel_specification = match kernel_specification { - KernelSpecification::Jupyter(spec) => spec, - KernelSpecification::PythonEnv(spec) => spec, - KernelSpecification::Remote(_spec) => { - // todo!(): Implement remote kernel specification - return Task::ready(Err(anyhow::anyhow!( - "Running remote kernels is not supported" - ))); - } - }; - + // todo: convert to weak view + session: View, + cx: &mut WindowContext, + ) -> Task>> { cx.spawn(|cx| async move { let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); let ports = peek_ports(ip).await?; @@ -304,7 +141,7 @@ impl RunningKernel { let mut cmd = kernel_specification.command(&connection_path)?; - let process = cmd + let mut process = cmd .current_dir(&working_directory) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) @@ -315,17 +152,13 @@ impl RunningKernel { let session_id = Uuid::new_v4().to_string(); - let mut iopub_socket = connection_info - .create_client_iopub_connection("", &session_id) - .await?; - let mut shell_socket = connection_info - .create_client_shell_connection(&session_id) - .await?; - let mut control_socket = connection_info - .create_client_control_connection(&session_id) - .await?; - - let (mut iopub, iosub) = futures::channel::mpsc::channel(100); + let mut iopub_socket = + runtimelib::create_client_iopub_connection(&connection_info, "", &session_id) + .await?; + let mut shell_socket = + runtimelib::create_client_shell_connection(&connection_info, &session_id).await?; + let mut control_socket = + runtimelib::create_client_control_connection(&connection_info, &session_id).await?; let (request_tx, mut request_rx) = futures::channel::mpsc::channel::(100); @@ -334,18 +167,41 @@ impl RunningKernel { let (mut shell_reply_tx, shell_reply_rx) = futures::channel::mpsc::channel(100); let mut messages_rx = SelectAll::new(); - messages_rx.push(iosub); messages_rx.push(control_reply_rx); messages_rx.push(shell_reply_rx); - let iopub_task = cx.background_executor().spawn({ - async move { - while let Ok(message) = iopub_socket.read().await { - iopub.send(message).await?; + cx.spawn({ + let session = session.clone(); + + |mut cx| async move { + while let Some(message) = messages_rx.next().await { + session + .update(&mut cx, |session, cx| { + session.route(&message, cx); + }) + .ok(); } anyhow::Ok(()) } - }); + }) + .detach(); + + // iopub task + cx.spawn({ + let session = session.clone(); + + |mut cx| async move { + while let Ok(message) = iopub_socket.read().await { + session + .update(&mut cx, |session, cx| { + session.route(&message, cx); + }) + .ok(); + } + anyhow::Ok(()) + } + }) + .detach(); let (mut control_request_tx, mut control_request_rx) = futures::channel::mpsc::channel(100); @@ -391,26 +247,118 @@ impl RunningKernel { } }); - anyhow::Ok(( - Self { - process, - request_tx, - working_directory, - _shell_task: shell_task, - _iopub_task: iopub_task, - _control_task: control_task, - _routing_task: routing_task, - connection_path, - execution_state: ExecutionState::Idle, - kernel_info: None, - }, - messages_rx, - )) + let stderr = process.stderr.take(); + + cx.spawn(|mut _cx| async move { + if stderr.is_none() { + return; + } + let reader = BufReader::new(stderr.unwrap()); + let mut lines = reader.lines(); + while let Some(Ok(line)) = lines.next().await { + log::error!("kernel: {}", line); + } + }) + .detach(); + + let stdout = process.stdout.take(); + + cx.spawn(|mut _cx| async move { + if stdout.is_none() { + return; + } + let reader = BufReader::new(stdout.unwrap()); + let mut lines = reader.lines(); + while let Some(Ok(line)) = lines.next().await { + log::info!("kernel: {}", line); + } + }) + .detach(); + + let status = process.status(); + + let process_status_task = cx.spawn(|mut cx| async move { + let error_message = match status.await { + Ok(status) => { + if status.success() { + log::info!("kernel process exited successfully"); + return; + } + + format!("kernel process exited with status: {:?}", status) + } + Err(err) => { + format!("kernel process exited with error: {:?}", err) + } + }; + + log::error!("{}", error_message); + + session + .update(&mut cx, |session, cx| { + session.kernel_errored(error_message, cx); + + cx.notify(); + }) + .ok(); + }); + + anyhow::Ok(Box::new(Self { + process, + request_tx, + working_directory, + _process_status_task: Some(process_status_task), + _shell_task: shell_task, + _control_task: control_task, + _routing_task: routing_task, + connection_path, + execution_state: ExecutionState::Idle, + kernel_info: None, + }) as Box) }) } } -impl Drop for RunningKernel { +impl RunningKernel for NativeRunningKernel { + fn request_tx(&self) -> mpsc::Sender { + self.request_tx.clone() + } + + fn working_directory(&self) -> &PathBuf { + &self.working_directory + } + + fn execution_state(&self) -> &ExecutionState { + &self.execution_state + } + + fn set_execution_state(&mut self, state: ExecutionState) { + self.execution_state = state; + } + + fn kernel_info(&self) -> Option<&KernelInfoReply> { + self.kernel_info.as_ref() + } + + fn set_kernel_info(&mut self, info: KernelInfoReply) { + self.kernel_info = Some(info); + } + + fn force_shutdown(&mut self, _cx: &mut WindowContext) -> Task> { + self._process_status_task.take(); + self.request_tx.close_channel(); + + Task::ready(match self.process.kill() { + Ok(_) => Ok(()), + Err(error) => Err(anyhow::anyhow!( + "Failed to kill the kernel process: {}", + error + )), + }) + } +} + +impl Drop for NativeRunningKernel { fn drop(&mut self) { std::fs::remove_file(&self.connection_path).ok(); self.request_tx.close_channel(); @@ -467,72 +415,6 @@ async fn read_kernels_dir(path: PathBuf, fs: &dyn Fs) -> Result, - worktree_id: WorktreeId, - cx: &mut AppContext, -) -> impl Future>> { - let python_language = LanguageName::new("Python"); - let toolchains = project - .read(cx) - .available_toolchains(worktree_id, python_language, cx); - let background_executor = cx.background_executor().clone(); - - async move { - let toolchains = if let Some(toolchains) = toolchains.await { - toolchains - } else { - return Ok(Vec::new()); - }; - - let kernelspecs = toolchains.toolchains.into_iter().map(|toolchain| { - background_executor.spawn(async move { - let python_path = toolchain.path.to_string(); - - // Check if ipykernel is installed - let ipykernel_check = Command::new(&python_path) - .args(&["-c", "import ipykernel"]) - .output() - .await; - - if ipykernel_check.is_ok() && ipykernel_check.unwrap().status.success() { - // Create a default kernelspec for this environment - let default_kernelspec = JupyterKernelspec { - argv: vec![ - python_path.clone(), - "-m".to_string(), - "ipykernel_launcher".to_string(), - "-f".to_string(), - "{connection_file}".to_string(), - ], - display_name: toolchain.name.to_string(), - language: "python".to_string(), - interrupt_mode: None, - metadata: None, - env: None, - }; - - Some(KernelSpecification::PythonEnv(LocalKernelSpecification { - name: toolchain.name.to_string(), - path: PathBuf::from(&python_path), - kernelspec: default_kernelspec, - })) - } else { - None - } - }) - }); - - let kernel_specs = futures::future::join_all(kernelspecs) - .await - .into_iter() - .flatten() - .collect(); - - anyhow::Ok(kernel_specs) - } -} - pub async fn local_kernel_specifications(fs: Arc) -> Result> { let mut data_dirs = dirs::data_dirs(); @@ -544,17 +426,11 @@ pub async fn local_kernel_specifications(fs: Arc) -> Result, + kernel_name: &str, + _path: &str, +) -> Result { + // + let kernel_launch_request = KernelLaunchRequest { + name: kernel_name.to_string(), + // Note: since the path we have locally may not be the same as the one on the remote server, + // we don't send it. We'll have to evaluate this decisiion along the way. + path: None, + }; + + let kernel_launch_request = serde_json::to_string(&kernel_launch_request)?; + + let request = Request::builder() + .method("POST") + .uri(&remote_server.api_url("/kernels")) + .header("Authorization", format!("token {}", remote_server.token)) + .body(AsyncBody::from(kernel_launch_request))?; + + let response = http_client.send(request).await?; + + if !response.status().is_success() { + let mut body = String::new(); + response.into_body().read_to_string(&mut body).await?; + return Err(anyhow::anyhow!("Failed to launch kernel: {}", body)); + } + + let mut body = String::new(); + response.into_body().read_to_string(&mut body).await?; + + let response: jupyter_websocket_client::Kernel = serde_json::from_str(&body)?; + + Ok(response.id) +} + +pub async fn list_remote_kernelspecs( + remote_server: RemoteServer, + http_client: Arc, +) -> Result> { + let url = remote_server.api_url("/kernelspecs"); + + let request = Request::builder() + .method("GET") + .uri(&url) + .header("Authorization", format!("token {}", remote_server.token)) + .body(AsyncBody::default())?; + + let response = http_client.send(request).await?; + + if response.status().is_success() { + let mut body = response.into_body(); + + let mut body_bytes = Vec::new(); + body.read_to_end(&mut body_bytes).await?; + + let kernel_specs: KernelSpecsResponse = serde_json::from_slice(&body_bytes)?; + + let remote_kernelspecs = kernel_specs + .kernelspecs + .into_iter() + .map(|(name, spec)| RemoteKernelSpecification { + name: name.clone(), + url: remote_server.base_url.clone(), + token: remote_server.token.clone(), + kernelspec: spec.spec, + }) + .collect::>(); + + if remote_kernelspecs.is_empty() { + Err(anyhow::anyhow!("No kernel specs found")) + } else { + Ok(remote_kernelspecs.clone()) + } + } else { + Err(anyhow::anyhow!( + "Failed to fetch kernel specs: {}", + response.status() + )) + } +} + +impl PartialEq for RemoteKernelSpecification { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.url == other.url + } +} + +impl Eq for RemoteKernelSpecification {} + +pub struct RemoteRunningKernel { + remote_server: RemoteServer, + _receiving_task: Task>, + _routing_task: Task>, + http_client: Arc, + pub working_directory: std::path::PathBuf, + pub request_tx: mpsc::Sender, + pub execution_state: ExecutionState, + pub kernel_info: Option, + pub kernel_id: String, +} + +impl RemoteRunningKernel { + pub fn new( + kernelspec: RemoteKernelSpecification, + working_directory: std::path::PathBuf, + session: View, + cx: &mut WindowContext, + ) -> Task>> { + let remote_server = RemoteServer { + base_url: kernelspec.url, + token: kernelspec.token, + }; + + let http_client = cx.http_client(); + + cx.spawn(|cx| async move { + let kernel_id = launch_remote_kernel( + &remote_server, + http_client.clone(), + &kernelspec.name, + working_directory.to_str().unwrap_or_default(), + ) + .await?; + + let (kernel_socket, _response) = remote_server.connect_to_kernel(&kernel_id).await?; + + let (mut w, mut r): (JupyterWebSocketWriter, JupyterWebSocketReader) = + kernel_socket.split(); + + let (request_tx, mut request_rx) = + futures::channel::mpsc::channel::(100); + + let routing_task = cx.background_executor().spawn({ + async move { + while let Some(message) = request_rx.next().await { + w.send(message).await.ok(); + } + Ok(()) + } + }); + + let receiving_task = cx.spawn({ + let session = session.clone(); + + |mut cx| async move { + while let Some(message) = r.next().await { + match message { + Ok(message) => { + session + .update(&mut cx, |session, cx| { + session.route(&message, cx); + }) + .ok(); + } + Err(e) => { + log::error!("Error receiving message: {:?}", e); + } + } + } + Ok(()) + } + }); + + anyhow::Ok(Box::new(Self { + _routing_task: routing_task, + _receiving_task: receiving_task, + remote_server, + working_directory, + request_tx, + // todo(kyle): pull this from the kernel API to start with + execution_state: ExecutionState::Idle, + kernel_info: None, + kernel_id, + http_client: http_client.clone(), + }) as Box) + }) + } +} + +impl Debug for RemoteRunningKernel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RemoteRunningKernel") + // custom debug that keeps tokens out of logs + .field("remote_server url", &self.remote_server.base_url) + .field("working_directory", &self.working_directory) + .field("request_tx", &self.request_tx) + .field("execution_state", &self.execution_state) + .field("kernel_info", &self.kernel_info) + .finish() + } +} + +impl RunningKernel for RemoteRunningKernel { + fn request_tx(&self) -> futures::channel::mpsc::Sender { + self.request_tx.clone() + } + + fn working_directory(&self) -> &std::path::PathBuf { + &self.working_directory + } + + fn execution_state(&self) -> &runtimelib::ExecutionState { + &self.execution_state + } + + fn set_execution_state(&mut self, state: runtimelib::ExecutionState) { + self.execution_state = state; + } + + fn kernel_info(&self) -> Option<&runtimelib::KernelInfoReply> { + self.kernel_info.as_ref() + } + + fn set_kernel_info(&mut self, info: runtimelib::KernelInfoReply) { + self.kernel_info = Some(info); + } + + fn force_shutdown(&mut self, cx: &mut WindowContext) -> Task> { + let url = self + .remote_server + .api_url(&format!("/kernels/{}", self.kernel_id)); + let token = self.remote_server.token.clone(); + let http_client = self.http_client.clone(); + + cx.spawn(|_| async move { + let request = Request::builder() + .method("DELETE") + .uri(&url) + .header("Authorization", format!("token {}", token)) + .body(AsyncBody::default())?; + + let response = http_client.send(request).await?; + + if response.status().is_success() { + Ok(()) + } else { + Err(anyhow::anyhow!( + "Failed to shutdown kernel: {}", + response.status() + )) + } + }) + } +} diff --git a/crates/repl/src/notebook/cell.rs b/crates/repl/src/notebook/cell.rs index 055e4c09f8..12d11853fb 100644 --- a/crates/repl/src/notebook/cell.rs +++ b/crates/repl/src/notebook/cell.rs @@ -114,7 +114,7 @@ impl Cell { id, metadata, source, - attachments: _, + .. } => { let source = source.join(""); diff --git a/crates/repl/src/repl.rs b/crates/repl/src/repl.rs index be187ff16f..4d11734e29 100644 --- a/crates/repl/src/repl.rs +++ b/crates/repl/src/repl.rs @@ -1,6 +1,6 @@ pub mod components; mod jupyter_settings; -mod kernels; +pub mod kernels; pub mod notebook; mod outputs; mod repl_editor; diff --git a/crates/repl/src/repl_store.rs b/crates/repl/src/repl_store.rs index a4863b809b..49c24bce68 100644 --- a/crates/repl/src/repl_store.rs +++ b/crates/repl/src/repl_store.rs @@ -7,11 +7,14 @@ use command_palette_hooks::CommandPaletteFilter; use gpui::{ prelude::*, AppContext, EntityId, Global, Model, ModelContext, Subscription, Task, View, }; +use jupyter_websocket_client::RemoteServer; use language::Language; use project::{Fs, Project, WorktreeId}; use settings::{Settings, SettingsStore}; -use crate::kernels::{local_kernel_specifications, python_env_kernel_specifications}; +use crate::kernels::{ + list_remote_kernelspecs, local_kernel_specifications, python_env_kernel_specifications, +}; use crate::{JupyterSettings, KernelSpecification, Session}; struct GlobalReplStore(Model); @@ -141,19 +144,50 @@ impl ReplStore { }) } + fn get_remote_kernel_specifications( + &self, + cx: &mut ModelContext, + ) -> Option>>> { + match ( + std::env::var("JUPYTER_SERVER"), + std::env::var("JUPYTER_TOKEN"), + ) { + (Ok(server), Ok(token)) => { + let remote_server = RemoteServer { + base_url: server, + token, + }; + let http_client = cx.http_client(); + Some(cx.spawn(|_, _| async move { + list_remote_kernelspecs(remote_server, http_client) + .await + .map(|specs| specs.into_iter().map(KernelSpecification::Remote).collect()) + })) + } + _ => None, + } + } + pub fn refresh_kernelspecs(&mut self, cx: &mut ModelContext) -> Task> { let local_kernel_specifications = local_kernel_specifications(self.fs.clone()); - cx.spawn(|this, mut cx| async move { - let local_kernel_specifications = local_kernel_specifications.await?; + let remote_kernel_specifications = self.get_remote_kernel_specifications(cx); - let mut kernel_options = Vec::new(); - for kernel_specification in local_kernel_specifications { - kernel_options.push(KernelSpecification::Jupyter(kernel_specification)); + cx.spawn(|this, mut cx| async move { + let mut all_specs = local_kernel_specifications + .await? + .into_iter() + .map(KernelSpecification::Jupyter) + .collect::>(); + + if let Some(remote_task) = remote_kernel_specifications { + if let Ok(remote_specs) = remote_task.await { + all_specs.extend(remote_specs); + } } this.update(&mut cx, |this, cx| { - this.kernel_specifications = kernel_options; + this.kernel_specifications = all_specs; cx.notify(); }) }) @@ -224,8 +258,9 @@ impl ReplStore { runtime_specification.kernelspec.language.to_lowercase() == language_at_cursor.code_fence_block_name().to_lowercase() } - KernelSpecification::Remote(_) => { - unimplemented!() + KernelSpecification::Remote(remote_spec) => { + remote_spec.kernelspec.language.to_lowercase() + == language_at_cursor.code_fence_block_name().to_lowercase() } }) .cloned() diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs index 74ce497572..0c1dc287ed 100644 --- a/crates/repl/src/session.rs +++ b/crates/repl/src/session.rs @@ -1,7 +1,8 @@ use crate::components::KernelListItem; +use crate::kernels::RemoteRunningKernel; use crate::setup_editor_session_actions; use crate::{ - kernels::{Kernel, KernelSpecification, RunningKernel}, + kernels::{Kernel, KernelSpecification, NativeRunningKernel}, outputs::{ExecutionStatus, ExecutionView}, KernelStatus, }; @@ -15,8 +16,7 @@ use editor::{ scroll::Autoscroll, Anchor, AnchorRangeExt as _, Editor, MultiBuffer, ToPoint, }; -use futures::io::BufReader; -use futures::{AsyncBufReadExt as _, FutureExt as _, StreamExt as _}; +use futures::FutureExt as _; use gpui::{ div, prelude::*, EventEmitter, Model, Render, Subscription, Task, View, ViewContext, WeakView, }; @@ -29,14 +29,13 @@ use runtimelib::{ use std::{env::temp_dir, ops::Range, sync::Arc, time::Duration}; use theme::ActiveTheme; use ui::{prelude::*, IconButtonShape, Tooltip}; +use util::ResultExt as _; pub struct Session { fs: Arc, editor: WeakView, pub kernel: Kernel, blocks: HashMap, - messaging_task: Option>, - process_status_task: Option>, pub kernel_specification: KernelSpecification, telemetry: Arc, _buffer_subscription: Subscription, @@ -219,8 +218,6 @@ impl Session { fs, editor, kernel: Kernel::StartingKernel(Task::ready(()).shared()), - messaging_task: None, - process_status_task: None, blocks: HashMap::default(), kernel_specification, _buffer_subscription: subscription, @@ -246,132 +243,40 @@ impl Session { cx.entity_id().to_string(), ); - let kernel = RunningKernel::new( - self.kernel_specification.clone(), - entity_id, - working_directory, - self.fs.clone(), - cx, - ); + let session_view = cx.view().clone(); + + let kernel = match self.kernel_specification.clone() { + KernelSpecification::Jupyter(kernel_specification) + | KernelSpecification::PythonEnv(kernel_specification) => NativeRunningKernel::new( + kernel_specification, + entity_id, + working_directory, + self.fs.clone(), + session_view, + cx, + ), + KernelSpecification::Remote(remote_kernel_specification) => RemoteRunningKernel::new( + remote_kernel_specification, + working_directory, + session_view, + cx, + ), + }; let pending_kernel = cx .spawn(|this, mut cx| async move { let kernel = kernel.await; match kernel { - Ok((mut kernel, mut messages_rx)) => { + Ok(kernel) => { this.update(&mut cx, |session, cx| { - let stderr = kernel.process.stderr.take(); - - cx.spawn(|_session, mut _cx| async move { - if stderr.is_none() { - return; - } - let reader = BufReader::new(stderr.unwrap()); - let mut lines = reader.lines(); - while let Some(Ok(line)) = lines.next().await { - // todo!(): Log stdout and stderr to something the session can show - log::error!("kernel: {}", line); - } - }) - .detach(); - - let stdout = kernel.process.stdout.take(); - - cx.spawn(|_session, mut _cx| async move { - if stdout.is_none() { - return; - } - let reader = BufReader::new(stdout.unwrap()); - let mut lines = reader.lines(); - while let Some(Ok(line)) = lines.next().await { - log::info!("kernel: {}", line); - } - }) - .detach(); - - let status = kernel.process.status(); session.kernel(Kernel::RunningKernel(kernel), cx); - - let process_status_task = cx.spawn(|session, mut cx| async move { - let error_message = match status.await { - Ok(status) => { - if status.success() { - log::info!("kernel process exited successfully"); - return; - } - - format!("kernel process exited with status: {:?}", status) - } - Err(err) => { - format!("kernel process exited with error: {:?}", err) - } - }; - - log::error!("{}", error_message); - - session - .update(&mut cx, |session, cx| { - session.kernel( - Kernel::ErroredLaunch(error_message.clone()), - cx, - ); - - session.blocks.values().for_each(|block| { - block.execution_view.update( - cx, - |execution_view, cx| { - match execution_view.status { - ExecutionStatus::Finished => { - // Do nothing when the output was good - } - _ => { - // All other cases, set the status to errored - execution_view.status = - ExecutionStatus::KernelErrored( - error_message.clone(), - ) - } - } - cx.notify(); - }, - ); - }); - - cx.notify(); - }) - .ok(); - }); - - session.process_status_task = Some(process_status_task); - - session.messaging_task = Some(cx.spawn(|session, mut cx| async move { - while let Some(message) = messages_rx.next().await { - session - .update(&mut cx, |session, cx| { - session.route(&message, cx); - }) - .ok(); - } - })); - - // todo!(@rgbkrk): send KernelInfoRequest once our shell channel read/writes are split - // cx.spawn(|this, mut cx| async move { - // cx.background_executor() - // .timer(Duration::from_millis(120)) - // .await; - // this.update(&mut cx, |this, cx| { - // this.send(KernelInfoRequest {}.into(), cx).ok(); - // }) - // .ok(); - // }) - // .detach(); }) .ok(); } Err(err) => { this.update(&mut cx, |session, cx| { - session.kernel(Kernel::ErroredLaunch(err.to_string()), cx); + session.kernel_errored(err.to_string(), cx); }) .ok(); } @@ -383,6 +288,26 @@ impl Session { cx.notify(); } + pub fn kernel_errored(&mut self, error_message: String, cx: &mut ViewContext) { + self.kernel(Kernel::ErroredLaunch(error_message.clone()), cx); + + self.blocks.values().for_each(|block| { + block.execution_view.update(cx, |execution_view, cx| { + match execution_view.status { + ExecutionStatus::Finished => { + // Do nothing when the output was good + } + _ => { + // All other cases, set the status to errored + execution_view.status = + ExecutionStatus::KernelErrored(error_message.clone()) + } + } + cx.notify(); + }); + }); + } + fn on_buffer_event( &mut self, buffer: Model, @@ -416,7 +341,7 @@ impl Session { fn send(&mut self, message: JupyterMessage, _cx: &mut ViewContext) -> anyhow::Result<()> { if let Kernel::RunningKernel(kernel) = &mut self.kernel { - kernel.request_tx.try_send(message).ok(); + kernel.request_tx().try_send(message).ok(); } anyhow::Ok(()) @@ -553,7 +478,7 @@ impl Session { } } - fn route(&mut self, message: &JupyterMessage, cx: &mut ViewContext) { + pub fn route(&mut self, message: &JupyterMessage, cx: &mut ViewContext) { let parent_message_id = match message.parent_header.as_ref() { Some(header) => &header.msg_id, None => return, @@ -631,23 +556,19 @@ impl Session { match kernel { Kernel::RunningKernel(mut kernel) => { - let mut request_tx = kernel.request_tx.clone(); + let mut request_tx = kernel.request_tx().clone(); + + let forced = kernel.force_shutdown(cx); cx.spawn(|this, mut cx| async move { let message: JupyterMessage = ShutdownRequest { restart: false }.into(); request_tx.try_send(message).ok(); + forced.await.log_err(); + // Give the kernel a bit of time to clean up cx.background_executor().timer(Duration::from_secs(3)).await; - this.update(&mut cx, |session, _cx| { - session.messaging_task.take(); - session.process_status_task.take(); - }) - .ok(); - - kernel.process.kill().ok(); - this.update(&mut cx, |session, cx| { session.clear_outputs(cx); session.kernel(Kernel::Shutdown, cx); @@ -658,8 +579,6 @@ impl Session { .detach(); } _ => { - self.messaging_task.take(); - self.process_status_task.take(); self.kernel(Kernel::Shutdown, cx); } } @@ -674,7 +593,9 @@ impl Session { // Do nothing if already restarting } Kernel::RunningKernel(mut kernel) => { - let mut request_tx = kernel.request_tx.clone(); + let mut request_tx = kernel.request_tx().clone(); + + let forced = kernel.force_shutdown(cx); cx.spawn(|this, mut cx| async move { // Send shutdown request with restart flag @@ -682,17 +603,11 @@ impl Session { let message: JupyterMessage = ShutdownRequest { restart: true }.into(); request_tx.try_send(message).ok(); - this.update(&mut cx, |session, _cx| { - session.messaging_task.take(); - session.process_status_task.take(); - }) - .ok(); - // Wait for kernel to shutdown cx.background_executor().timer(Duration::from_secs(1)).await; // Force kill the kernel if it hasn't shut down - kernel.process.kill().ok(); + forced.await.log_err(); // Start a new kernel this.update(&mut cx, |session, cx| { @@ -705,9 +620,6 @@ impl Session { .detach(); } _ => { - // If it's not already running, we can just clean up and start a new kernel - self.messaging_task.take(); - self.process_status_task.take(); self.clear_outputs(cx); self.start_kernel(cx); } @@ -727,7 +639,7 @@ impl Render for Session { let (status_text, interrupt_button) = match &self.kernel { Kernel::RunningKernel(kernel) => ( kernel - .kernel_info + .kernel_info() .as_ref() .map(|info| info.language_info.name.clone()), Some( @@ -747,7 +659,7 @@ impl Render for Session { KernelListItem::new(self.kernel_specification.clone()) .status_color(match &self.kernel { - Kernel::RunningKernel(kernel) => match kernel.execution_state { + Kernel::RunningKernel(kernel) => match kernel.execution_state() { ExecutionState::Idle => Color::Success, ExecutionState::Busy => Color::Modified, }, diff --git a/crates/rich_text/src/rich_text.rs b/crates/rich_text/src/rich_text.rs index 80b7786c24..df830419d3 100644 --- a/crates/rich_text/src/rich_text.rs +++ b/crates/rich_text/src/rich_text.rs @@ -310,12 +310,7 @@ pub fn render_markdown_mut( } Event::Start(tag) => match tag { Tag::Paragraph => new_paragraph(text, &mut list_stack), - Tag::Heading { - level: _, - id: _, - classes: _, - attrs: _, - } => { + Tag::Heading { .. } => { new_paragraph(text, &mut list_stack); bold_depth += 1; } @@ -333,12 +328,7 @@ pub fn render_markdown_mut( Tag::Emphasis => italic_depth += 1, Tag::Strong => bold_depth += 1, Tag::Strikethrough => strikethrough_depth += 1, - Tag::Link { - link_type: _, - dest_url, - title: _, - id: _, - } => link_url = Some(dest_url.to_string()), + Tag::Link { dest_url, .. } => link_url = Some(dest_url.to_string()), Tag::List(number) => { list_stack.push((number, false)); } diff --git a/crates/rope/src/chunk.rs b/crates/rope/src/chunk.rs index c158d2429e..5c2b9b87c3 100644 --- a/crates/rope/src/chunk.rs +++ b/crates/rope/src/chunk.rs @@ -504,8 +504,6 @@ impl<'a> ChunkSlice<'a> { #[inline(always)] pub fn tabs(&self) -> Tabs { Tabs { - byte_offset: 0, - char_offset: 0, tabs: self.tabs, chars: self.chars, } @@ -513,8 +511,6 @@ impl<'a> ChunkSlice<'a> { } pub struct Tabs { - byte_offset: usize, - char_offset: usize, tabs: u128, chars: u128, } @@ -536,21 +532,14 @@ impl Iterator for Tabs { let tab_offset = self.tabs.trailing_zeros() as usize; let chars_mask = (1 << tab_offset) - 1; let char_offset = (self.chars & chars_mask).count_ones() as usize; - self.byte_offset += tab_offset; - self.char_offset += char_offset; - let position = TabPosition { - byte_offset: self.byte_offset, - char_offset: self.char_offset, - }; - self.byte_offset += 1; - self.char_offset += 1; - if self.byte_offset == MAX_BASE { - self.tabs = 0; - } else { - self.tabs >>= tab_offset + 1; - self.chars >>= tab_offset + 1; - } + // Since tabs are 1 byte the tab offset is the same as the byte offset + let position = TabPosition { + byte_offset: tab_offset, + char_offset: char_offset, + }; + // Remove the tab we've just seen + self.tabs ^= 1 << tab_offset; Some(position) } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 1f4492d992..8430fd1f37 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -536,7 +536,7 @@ impl Item for ProjectSearchView { } } - fn breadcrumb_location(&self) -> ToolbarItemLocation { + fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation { if self.has_matches() { ToolbarItemLocation::Secondary } else { diff --git a/crates/semantic_index/examples/index.rs b/crates/semantic_index/examples/index.rs index 2efd94cb57..25e03f5b3a 100644 --- a/crates/semantic_index/examples/index.rs +++ b/crates/semantic_index/examples/index.rs @@ -25,7 +25,7 @@ fn main() { store.update_user_settings::(cx, |_| {}); }); - let clock = Arc::new(FakeSystemClock::default()); + let clock = Arc::new(FakeSystemClock::new()); let http = Arc::new(HttpClientWithUrl::new( Arc::new( diff --git a/crates/semantic_index/src/embedding_index.rs b/crates/semantic_index/src/embedding_index.rs index 0913124341..4e3d74a2ea 100644 --- a/crates/semantic_index/src/embedding_index.rs +++ b/crates/semantic_index/src/embedding_index.rs @@ -7,6 +7,7 @@ use anyhow::{anyhow, Context as _, Result}; use collections::Bound; use feature_flags::FeatureFlagAppExt; use fs::Fs; +use fs::MTime; use futures::stream::StreamExt; use futures_batch::ChunksTimeoutStreamExt; use gpui::{AppContext, Model, Task}; @@ -17,14 +18,7 @@ use project::{Entry, UpdatedEntriesSet, Worktree}; use serde::{Deserialize, Serialize}; use smol::channel; use smol::future::FutureExt; -use std::{ - cmp::Ordering, - future::Future, - iter, - path::Path, - sync::Arc, - time::{Duration, SystemTime}, -}; +use std::{cmp::Ordering, future::Future, iter, path::Path, sync::Arc, time::Duration}; use util::ResultExt; use worktree::Snapshot; @@ -451,7 +445,7 @@ struct ChunkFiles { pub struct ChunkedFile { pub path: Arc, - pub mtime: Option, + pub mtime: Option, pub handle: IndexingEntryHandle, pub text: String, pub chunks: Vec, @@ -465,7 +459,7 @@ pub struct EmbedFiles { #[derive(Debug, Serialize, Deserialize)] pub struct EmbeddedFile { pub path: Arc, - pub mtime: Option, + pub mtime: Option, pub chunks: Vec, } diff --git a/crates/semantic_index/src/summary_backlog.rs b/crates/semantic_index/src/summary_backlog.rs index c6d8e33a45..e77fa4862f 100644 --- a/crates/semantic_index/src/summary_backlog.rs +++ b/crates/semantic_index/src/summary_backlog.rs @@ -1,5 +1,6 @@ use collections::HashMap; -use std::{path::Path, sync::Arc, time::SystemTime}; +use fs::MTime; +use std::{path::Path, sync::Arc}; const MAX_FILES_BEFORE_RESUMMARIZE: usize = 4; const MAX_BYTES_BEFORE_RESUMMARIZE: u64 = 1_000_000; // 1 MB @@ -7,14 +8,14 @@ const MAX_BYTES_BEFORE_RESUMMARIZE: u64 = 1_000_000; // 1 MB #[derive(Default, Debug)] pub struct SummaryBacklog { /// Key: path to a file that needs summarization, but that we haven't summarized yet. Value: that file's size on disk, in bytes, and its mtime. - files: HashMap, (u64, Option)>, + files: HashMap, (u64, Option)>, /// Cache of the sum of all values in `files`, so we don't have to traverse the whole map to check if we're over the byte limit. total_bytes: u64, } impl SummaryBacklog { /// Store the given path in the backlog, along with how many bytes are in it. - pub fn insert(&mut self, path: Arc, bytes_on_disk: u64, mtime: Option) { + pub fn insert(&mut self, path: Arc, bytes_on_disk: u64, mtime: Option) { let (prev_bytes, _) = self .files .insert(path, (bytes_on_disk, mtime)) @@ -34,7 +35,7 @@ impl SummaryBacklog { /// Remove all the entries in the backlog and return the file paths as an iterator. #[allow(clippy::needless_lifetimes)] // Clippy thinks this 'a can be elided, but eliding it gives a compile error - pub fn drain<'a>(&'a mut self) -> impl Iterator, Option)> + 'a { + pub fn drain<'a>(&'a mut self) -> impl Iterator, Option)> + 'a { self.total_bytes = 0; self.files diff --git a/crates/semantic_index/src/summary_index.rs b/crates/semantic_index/src/summary_index.rs index 1cbb670397..44cac88564 100644 --- a/crates/semantic_index/src/summary_index.rs +++ b/crates/semantic_index/src/summary_index.rs @@ -1,6 +1,6 @@ use anyhow::{anyhow, Context as _, Result}; use arrayvec::ArrayString; -use fs::Fs; +use fs::{Fs, MTime}; use futures::{stream::StreamExt, TryFutureExt}; use futures_batch::ChunksTimeoutStreamExt; use gpui::{AppContext, Model, Task}; @@ -21,7 +21,7 @@ use std::{ future::Future, path::Path, sync::Arc, - time::{Duration, Instant, SystemTime}, + time::{Duration, Instant}, }; use util::ResultExt; use worktree::Snapshot; @@ -39,7 +39,7 @@ struct UnsummarizedFile { // Path to the file on disk path: Arc, // The mtime of the file on disk - mtime: Option, + mtime: Option, // BLAKE3 hash of the source file's contents digest: Blake3Digest, // The source file's contents @@ -51,7 +51,7 @@ struct SummarizedFile { // Path to the file on disk path: String, // The mtime of the file on disk - mtime: Option, + mtime: Option, // BLAKE3 hash of the source file's contents digest: Blake3Digest, // The LLM's summary of the file's contents @@ -63,7 +63,7 @@ pub type Blake3Digest = ArrayString<{ blake3::OUT_LEN * 2 }>; #[derive(Debug, Serialize, Deserialize)] pub struct FileDigest { - pub mtime: Option, + pub mtime: Option, pub digest: Blake3Digest, } @@ -88,7 +88,7 @@ pub struct SummaryIndex { } struct Backlogged { - paths_to_digest: channel::Receiver, Option)>>, + paths_to_digest: channel::Receiver, Option)>>, task: Task>, } @@ -319,7 +319,7 @@ impl SummaryIndex { digest_db: heed::Database>, txn: &RoTxn<'_>, entry: &Entry, - ) -> Vec<(Arc, Option)> { + ) -> Vec<(Arc, Option)> { let entry_db_key = db_key_for_path(&entry.path); match digest_db.get(&txn, &entry_db_key) { @@ -414,7 +414,7 @@ impl SummaryIndex { fn digest_files( &self, - paths: channel::Receiver, Option)>>, + paths: channel::Receiver, Option)>>, worktree_abs_path: Arc, cx: &AppContext, ) -> MightNeedSummaryFiles { @@ -646,7 +646,7 @@ impl SummaryIndex { let start = Instant::now(); let backlogged = { let (tx, rx) = channel::bounded(512); - let needs_summary: Vec<(Arc, Option)> = { + let needs_summary: Vec<(Arc, Option)> = { let mut backlog = self.backlog.lock(); backlog.drain().collect() diff --git a/crates/settings/src/key_equivalents.rs b/crates/settings/src/key_equivalents.rs index 1c68f48db4..4c5ae9e065 100644 --- a/crates/settings/src/key_equivalents.rs +++ b/crates/settings/src/key_equivalents.rs @@ -26,157 +26,1377 @@ use collections::HashMap; // From there I used multi-cursor to produce this match statement. #[cfg(target_os = "macos")] pub fn get_key_equivalents(layout: &str) -> Option> { - let (from, to) = match layout { - "com.apple.keylayout.Welsh" => ("#", "£"), - "com.apple.keylayout.Turkmen" => ("qc]Q`|[XV\\^v~Cx}{", "äçöÄžŞňÜÝş№ýŽÇüÖŇ"), - "com.apple.keylayout.Turkish-QWERTY-PC" => ( - "$\\|`'[}^=.#{*+:/~;)(@<,&]>\"", - "+,;<ığÜ&.ç^Ğ(:Ş*>ş=)'Öö/üÇI", - ), - "com.apple.keylayout.Sami-PC" => ( - "}*x\"w[~^/@`]{|<)>W(\\X=Qq&':;", - "Æ(čŊšøŽ&´\"žæØĐ;=:Š)đČ`Áá/ŋÅå", - ), - "com.apple.keylayout.LatinAmerican" => { - ("[^~>`(<\\@{;*&/):]|='}\"", "{&>:<);¿\"[ñ(/'=Ñ}¡*´]¨") - } - "com.apple.keylayout.IrishExtended" => ("#", "£"), - "com.apple.keylayout.Icelandic" => ("[}=:/'){(*&;^|`\"\\>]<~@", "æ´*Ð'ö=Æ)(/ð&Þ<Öþ:´;>\""), - "com.apple.keylayout.German-DIN-2137" => { - ("}~/<^>{`:\\)&=[]@|;#'\"(*", "Ä>ß;&:Ö<Ü#=/*öä\"'ü§´`)(") - } - "com.apple.keylayout.FinnishSami-PC" => { - (")=*\"\\[@{:>';/<|~(]}^`&", "=`(ˆ@ö\"ÖÅ:¨å´;*>)äÄ& { - ("];{`:'*<~=/}\\|&[\"($^)>@", "äåÖ<Ũ(;>`´Ä'*/öˆ)€&=:\"") - } - "com.apple.keylayout.Faroese" => ("}\";/$>^@~`:&[*){|]=(\\<'", "ÐØæ´€:&\"><Æ/å(=Å*ð`)';ø"), - "com.apple.keylayout.Croatian-PC" => { - ("{@~;<=>(&*['|]\":/}^`)\\", "Š\">č;*:)/(šćŽđĆČ'Đ&<=ž") - } - "com.apple.keylayout.Croatian" => ("{@;<~=>(&*['|]\":}^)\\`", "Š\"č;>*:)'(šćŽđĆČĐ&=ž<"), - "com.apple.keylayout.Azeri" => (":{W?./\"[}<]|,>';w", "IÖÜ,ş.ƏöĞÇğ/çŞəıü"), - "com.apple.keylayout.Albanian" => ("\\'~;:|<>`\"@", "ë@>çÇË;:<'\""), - "com.apple.keylayout.SwissFrench" => ( - ":@&'~^)$;\"][\\/#={!|*+`<(>}", - "ü\"/^>&=çè`àé$'*¨ö+£(!<;):ä", - ), - "com.apple.keylayout.Swedish" => ("(]\\\"~$`^{|/>*:;<)&=[}'@", ")ä'^>€<&Ö*´:(Åå;=/`öĨ\""), - "com.apple.keylayout.Swedish-Pro" => { - ("/^*`'{|)$>&<[\\;(~\"}@]:=", "´&(<¨Ö*=€:/;ö'å)>^Ä\"äÅ`") - } - "com.apple.keylayout.Spanish" => ("|!\\<{[:;@`/~].'>}\"^", "\"¡'¿Ññº´!<.>;ç`Ç:¨/"), - "com.apple.keylayout.Spanish-ISO" => ( - "|~`]/:)(<&^>*;#}\"{.\\['@", - "\"><;.º=)¿/&Ç(´·not found¨Ñç'ñ`\"", - ), - "com.apple.keylayout.Portuguese" => (")`/'^\"<];>[:{@}(&*=~", "=<'´&`;~º:çªÇ\"^)/(*>"), - "com.apple.keylayout.Italian" => ( - "*7};8:!5%(1&4]^\\6)32>.à32", - ), - "com.apple.keylayout.Italian-Pro" => { - ("/:@[]'\\=){;|#<\"(*^&`}>~", "'é\"òàìù*=çè§£;^)(&/<°:>") - } - "com.apple.keylayout.Irish" => ("#", "£"), - "com.apple.keylayout.German" => ("=`#'}:)/\"^&]*{;|[<(>~@\\", "*<§´ÄÜ=ß`&/ä(Öü'ö;):>\"#"), - "com.apple.keylayout.French" => ( - "*}7;8:!5%(1&4]\\^6)32>.ç32", - ), - "com.apple.keylayout.French-numerical" => ( - "|!52;][>&@\"%'{)<~7.1/^(}*8#0$9`6\\3:4", - "£1(é)$^/72%5ù¨0.>è;&:69*8!3à4ç<§`\"°'", - ), - "com.apple.keylayout.French-PC" => ( - "!&\"_$}/72>8]#:31)*<%4;6\\-{['@(0|5.`9~^", - "17%°4£:èé/_$3§\"&08.5'!-*)¨^ù29àμ(;<ç>6", - ), - "com.apple.keylayout.Finnish" => ("/^*`)'{|$>&<[\\~;(\"}@]:=", "´&(<=¨Ö*€:/;ö'>å)^Ä\"äÅ`"), - "com.apple.keylayout.Danish" => ("=[;'`{}|>]*^(&@~)<\\/$\":", "`æå¨<ÆØ*:ø(&)/\">=;'´€^Å"), - "com.apple.keylayout.Canadian-CSA" => ("\\?']/><[{}|~`\"", "àÉèçé\"'^¨ÇÀÙùÈ"), - "com.apple.keylayout.British" => ("#", "£"), - "com.apple.keylayout.Brazilian-ABNT2" => ("\"|~?`'/^\\", "`^\"Ç'´ç¨~"), - "com.apple.keylayout.Belgian" => ( - "`3/*<\\8>7#&96@);024(|'1\":$[~5.%^}]{!", - "<\":8.`!/è37ç§20)àé'9£ù&%°4^>(;56*$¨1", - ), - "com.apple.keylayout.Austrian" => ("/^*`'{|)>&<[\\;(~\"}@]:=#", "ß&(<´Ö'=:/;ö#ü)>`Ä\"äÜ*§"), - "com.apple.keylayout.Slovak-QWERTY" => ( - "):9;63'\"]^/+@~>`? ( - "!$`10&:#4^*~{%5')}6/\"[8]97?;<@23>(+", - "14ň+é7\"3č68ŇÚ5ť§0Äž'!úáäíýˇô?2ľš:9%", - ), - "com.apple.keylayout.Polish" => ( - "&)|?,%:;^}]_{!+#(*`/[~<\"$.>'@=\\", - ":\"$Ż.+Łł=)(ćź§]!/_<żó>śę?,ńą%[;", - ), - "com.apple.keylayout.Lithuanian" => ("+#&=!%1*@73^584$26", "ŽĘŲžĄĮąŪČųęŠįūėĖčš"), - "com.apple.keylayout.Hungarian" => ( - "}(*@\"{=/|;>'[`<~\\!$&0#:]^)+", - "Ú)(\"ÁŐóüŰé:áőíÜÍű'!=ö+Éú/ÖÓ", - ), - "com.apple.keylayout.Hungarian-QWERTY" => ( - "=]#>@/&<`0')~(\\!:*;$\"+^{|}[", - "óú+:\"ü=ÜíöáÖÍ)ű'É(é!ÁÓ/ŐŰÚő", - ), - "com.apple.keylayout.Czech-QWERTY" => ( - "9>0[2()\"}@]46%5;#8{*7^~+!3?&'<$/1`:", - "í:éúě90!(2)čž5řů3áÚ8ý6`%1šˇ7§?4'+¨\"", - ), - "com.apple.keylayout.Maltese" => ("[`}{#]~", "ġżĦĠ£ħŻ"), - "com.apple.keylayout.Turkish" => ( - "|}(#>&^-/`$%@]~*,[\"<_.{:'\\)", - "ÜI%\"Ç)/ş.<'(*ı>_öğ-ÖŞçĞ$,ü:", - ), - "com.apple.keylayout.Turkish-Standard" => { - ("|}(#>=&^`@]~*,;[\"<.{:'\\)", "ÜI)^;*'&ö\"ıÖ(.çğŞ:,ĞÇşü=") - } - "com.apple.keylayout.NorwegianSami-PC" => { - ("\"}~<`&>':{@*^|\\)=([]/;", "ˆÆ>; { - (";\\@>&'<]\"|(=}^)`[~:*{", "čž\":'ć;đĆŽ)*Đ&=<š>Č(Š") - } - "com.apple.keylayout.Slovenian" => ("]`^@)&\":'*=<{;}(~>\\|[", "đ<&\"='ĆČć(*;ŠčĐ)>:žŽš"), - "com.apple.keylayout.SwedishSami-PC" => { - ("@=<^|`>){'&\"}]~[/:*\\(;", "\"`;&*<:=Ö¨/ˆÄä>ö´Å(@)å") - } - "com.apple.keylayout.SwissGerman" => ( - "={#:\\}!(+]/<\";$'`*[>&^~@)|", - "¨é*è$à+)!ä';`üç^<(ö:/&>\"=£", - ), - "com.apple.keylayout.Hawaiian" => ("'", "ʻ"), - "com.apple.keylayout.NorthernSami" => ( - ":/[<{X\"wQx\\(;~>W}`*@])'^|=q&", - "Å´ø;ØČŊšÁčđ)åŽ:ŠÆž(\"æ=ŋ&Đ`á/", - ), - "com.apple.keylayout.USInternational-PC" => ("^~", "ˆ˜"), - "com.apple.keylayout.NorwegianExtended" => ("^~", "ˆ˜"), - "com.apple.keylayout.Norwegian" => ("`'~\"\\*|=/@)[:}&><]{(^;", "<¨>^@(*`´\"=øÅÆ/:;æØ)&å"), - "com.apple.keylayout.ABC-QWERTZ" => { - ("\"}~<`>'&#:{@*^|\\)=(]/;[", "`Ä>;<:´/§ÜÖ\"(&'#=*)äßüö") - } - "com.apple.keylayout.ABC-AZERTY" => ( - ">[$61%@7|)&8\":}593(.4^8:ùà", - ), - "com.apple.keylayout.Czech" => ( - "(7*#193620?/{)@~!$8+;:%4\">`^]&5}[<'", - "9ý83+íšžěéˇ'Ú02`14á%ů\"5č!:¨6)7ř(ú?§", - ), - "com.apple.keylayout.Brazilian-Pro" => ("^~", "ˆ˜"), - _ => { - return None; - } - }; - debug_assert!(from.chars().count() == to.chars().count()); + let mappings: &[(char, char)] = match layout { + "com.apple.keylayout.ABC-AZERTY" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.ABC-QWERTZ" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Albanian" => &[ + ('"', '\''), + (':', 'Ç'), + (';', 'ç'), + ('<', ';'), + ('>', ':'), + ('@', '"'), + ('\'', '@'), + ('\\', 'ë'), + ('`', '<'), + ('|', 'Ë'), + ('~', '>'), + ], + "com.apple.keylayout.Austrian" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Azeri" => &[ + ('"', 'Ə'), + (',', 'ç'), + ('.', 'ş'), + ('/', '.'), + (':', 'I'), + (';', 'ı'), + ('<', 'Ç'), + ('>', 'Ş'), + ('?', ','), + ('W', 'Ü'), + ('[', 'ö'), + ('\'', 'ə'), + (']', 'ğ'), + ('w', 'ü'), + ('{', 'Ö'), + ('|', '/'), + ('}', 'Ğ'), + ], + "com.apple.keylayout.Belgian" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.Brazilian-ABNT2" => &[ + ('"', '`'), + ('/', 'ç'), + ('?', 'Ç'), + ('\'', '´'), + ('\\', '~'), + ('^', '¨'), + ('`', '\''), + ('|', '^'), + ('~', '"'), + ], + "com.apple.keylayout.Brazilian-Pro" => &[('^', 'ˆ'), ('~', '˜')], + "com.apple.keylayout.British" => &[('#', '£')], + "com.apple.keylayout.Canadian-CSA" => &[ + ('"', 'È'), + ('/', 'é'), + ('<', '\''), + ('>', '"'), + ('?', 'É'), + ('[', '^'), + ('\'', 'è'), + ('\\', 'à'), + (']', 'ç'), + ('`', 'ù'), + ('{', '¨'), + ('|', 'À'), + ('}', 'Ç'), + ('~', 'Ù'), + ], + "com.apple.keylayout.Croatian" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Croatian-PC" => &[ + ('"', 'Ć'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Czech" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ě'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ř'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ů'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', ')'), + ('^', '6'), + ('`', '¨'), + ('{', 'Ú'), + ('}', '('), + ('~', '`'), + ], + "com.apple.keylayout.Czech-QWERTY" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ě'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ř'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ů'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', ')'), + ('^', '6'), + ('`', '¨'), + ('{', 'Ú'), + ('}', '('), + ('~', '`'), + ], + "com.apple.keylayout.Danish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'æ'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ø'), + ('^', '&'), + ('`', '<'), + ('{', 'Æ'), + ('|', '*'), + ('}', 'Ø'), + ('~', '>'), + ], + "com.apple.keylayout.Faroese" => &[ + ('"', 'Ø'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Æ'), + (';', 'æ'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'å'), + ('\'', 'ø'), + ('\\', '\''), + (']', 'ð'), + ('^', '&'), + ('`', '<'), + ('{', 'Å'), + ('|', '*'), + ('}', 'Ð'), + ('~', '>'), + ], + "com.apple.keylayout.Finnish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.FinnishExtended" => &[ + ('"', 'ˆ'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.FinnishSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '@'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.French" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.French-PC" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('-', ')'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '-'), + ('7', 'è'), + ('8', '_'), + ('9', 'ç'), + (':', '§'), + (';', '!'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '*'), + (']', '$'), + ('^', '6'), + ('_', '°'), + ('`', '<'), + ('{', '¨'), + ('|', 'μ'), + ('}', '£'), + ('~', '>'), + ], + "com.apple.keylayout.French-numerical" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.German" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.German-DIN-2137" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Hawaiian" => &[('\'', 'ʻ')], + "com.apple.keylayout.Hungarian" => &[ + ('!', '\''), + ('"', 'Á'), + ('#', '+'), + ('$', '!'), + ('&', '='), + ('(', ')'), + (')', 'Ö'), + ('*', '('), + ('+', 'Ó'), + ('/', 'ü'), + ('0', 'ö'), + (':', 'É'), + (';', 'é'), + ('<', 'Ü'), + ('=', 'ó'), + ('>', ':'), + ('@', '"'), + ('[', 'ő'), + ('\'', 'á'), + ('\\', 'ű'), + (']', 'ú'), + ('^', '/'), + ('`', 'í'), + ('{', 'Ő'), + ('|', 'Ű'), + ('}', 'Ú'), + ('~', 'Í'), + ], + "com.apple.keylayout.Hungarian-QWERTY" => &[ + ('!', '\''), + ('"', 'Á'), + ('#', '+'), + ('$', '!'), + ('&', '='), + ('(', ')'), + (')', 'Ö'), + ('*', '('), + ('+', 'Ó'), + ('/', 'ü'), + ('0', 'ö'), + (':', 'É'), + (';', 'é'), + ('<', 'Ü'), + ('=', 'ó'), + ('>', ':'), + ('@', '"'), + ('[', 'ő'), + ('\'', 'á'), + ('\\', 'ű'), + (']', 'ú'), + ('^', '/'), + ('`', 'í'), + ('{', 'Ő'), + ('|', 'Ű'), + ('}', 'Ú'), + ('~', 'Í'), + ], + "com.apple.keylayout.Icelandic" => &[ + ('"', 'Ö'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Ð'), + (';', 'ð'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'æ'), + ('\'', 'ö'), + ('\\', 'þ'), + (']', '´'), + ('^', '&'), + ('`', '<'), + ('{', 'Æ'), + ('|', 'Þ'), + ('}', '´'), + ('~', '>'), + ], + "com.apple.keylayout.Irish" => &[('#', '£')], + "com.apple.keylayout.IrishExtended" => &[('#', '£')], + "com.apple.keylayout.Italian" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + (',', ';'), + ('.', ':'), + ('/', ','), + ('0', 'é'), + ('1', '&'), + ('2', '"'), + ('3', '\''), + ('4', '('), + ('5', 'ç'), + ('6', 'è'), + ('7', ')'), + ('8', '£'), + ('9', 'à'), + (':', '!'), + (';', 'ò'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', 'ì'), + ('\'', 'ù'), + ('\\', '§'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '^'), + ('|', '°'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.Italian-Pro" => &[ + ('"', '^'), + ('#', '£'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'é'), + (';', 'è'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ò'), + ('\'', 'ì'), + ('\\', 'ù'), + (']', 'à'), + ('^', '&'), + ('`', '<'), + ('{', 'ç'), + ('|', '§'), + ('}', '°'), + ('~', '>'), + ], + "com.apple.keylayout.LatinAmerican" => &[ + ('"', '¨'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Ñ'), + (';', 'ñ'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', '{'), + ('\'', '´'), + ('\\', '¿'), + (']', '}'), + ('^', '&'), + ('`', '<'), + ('{', '['), + ('|', '¡'), + ('}', ']'), + ('~', '>'), + ], + "com.apple.keylayout.Lithuanian" => &[ + ('!', 'Ą'), + ('#', 'Ę'), + ('$', 'Ė'), + ('%', 'Į'), + ('&', 'Ų'), + ('*', 'Ū'), + ('+', 'Ž'), + ('1', 'ą'), + ('2', 'č'), + ('3', 'ę'), + ('4', 'ė'), + ('5', 'į'), + ('6', 'š'), + ('7', 'ų'), + ('8', 'ū'), + ('=', 'ž'), + ('@', 'Č'), + ('^', 'Š'), + ], + "com.apple.keylayout.Maltese" => &[ + ('#', '£'), + ('[', 'ġ'), + (']', 'ħ'), + ('`', 'ż'), + ('{', 'Ġ'), + ('}', 'Ħ'), + ('~', 'Ż'), + ], + "com.apple.keylayout.NorthernSami" => &[ + ('"', 'Ŋ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('Q', 'Á'), + ('W', 'Š'), + ('X', 'Č'), + ('[', 'ø'), + ('\'', 'ŋ'), + ('\\', 'đ'), + (']', 'æ'), + ('^', '&'), + ('`', 'ž'), + ('q', 'á'), + ('w', 'š'), + ('x', 'č'), + ('{', 'Ø'), + ('|', 'Đ'), + ('}', 'Æ'), + ('~', 'Ž'), + ], + "com.apple.keylayout.Norwegian" => &[ + ('"', '^'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\'', '¨'), + ('\\', '@'), + (']', 'æ'), + ('^', '&'), + ('`', '<'), + ('{', 'Ø'), + ('|', '*'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.NorwegianExtended" => &[('^', 'ˆ'), ('~', '˜')], + "com.apple.keylayout.NorwegianSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\'', '¨'), + ('\\', '@'), + (']', 'æ'), + ('^', '&'), + ('`', '<'), + ('{', 'Ø'), + ('|', '*'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.Polish" => &[ + ('!', '§'), + ('"', 'ę'), + ('#', '!'), + ('$', '?'), + ('%', '+'), + ('&', ':'), + ('(', '/'), + (')', '"'), + ('*', '_'), + ('+', ']'), + (',', '.'), + ('.', ','), + ('/', 'ż'), + (':', 'Ł'), + (';', 'ł'), + ('<', 'ś'), + ('=', '['), + ('>', 'ń'), + ('?', 'Ż'), + ('@', '%'), + ('[', 'ó'), + ('\'', 'ą'), + ('\\', ';'), + (']', '('), + ('^', '='), + ('_', 'ć'), + ('`', '<'), + ('{', 'ź'), + ('|', '$'), + ('}', ')'), + ('~', '>'), + ], + "com.apple.keylayout.Portuguese" => &[ + ('"', '`'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'ª'), + (';', 'º'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ç'), + ('\'', '´'), + (']', '~'), + ('^', '&'), + ('`', '<'), + ('{', 'Ç'), + ('}', '^'), + ('~', '>'), + ], + "com.apple.keylayout.Sami-PC" => &[ + ('"', 'Ŋ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('Q', 'Á'), + ('W', 'Š'), + ('X', 'Č'), + ('[', 'ø'), + ('\'', 'ŋ'), + ('\\', 'đ'), + (']', 'æ'), + ('^', '&'), + ('`', 'ž'), + ('q', 'á'), + ('w', 'š'), + ('x', 'č'), + ('{', 'Ø'), + ('|', 'Đ'), + ('}', 'Æ'), + ('~', 'Ž'), + ], + "com.apple.keylayout.Serbian-Latin" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Slovak" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ľ'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ť'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ô'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', 'ä'), + ('^', '6'), + ('`', 'ň'), + ('{', 'Ú'), + ('}', 'Ä'), + ('~', 'Ň'), + ], + "com.apple.keylayout.Slovak-QWERTY" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ľ'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ť'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ô'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', 'ä'), + ('^', '6'), + ('`', 'ň'), + ('{', 'Ú'), + ('}', 'Ä'), + ('~', 'Ň'), + ], + "com.apple.keylayout.Slovenian" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Spanish" => &[ + ('!', '¡'), + ('"', '¨'), + ('.', 'ç'), + ('/', '.'), + (':', 'º'), + (';', '´'), + ('<', '¿'), + ('>', 'Ç'), + ('@', '!'), + ('[', 'ñ'), + ('\'', '`'), + ('\\', '\''), + (']', ';'), + ('^', '/'), + ('`', '<'), + ('{', 'Ñ'), + ('|', '"'), + ('}', ':'), + ('~', '>'), + ], + "com.apple.keylayout.Spanish-ISO" => &[ + ('"', '¨'), + ('#', '·'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('.', 'ç'), + ('/', '.'), + (':', 'º'), + (';', '´'), + ('<', '¿'), + ('>', 'Ç'), + ('@', '"'), + ('[', 'ñ'), + ('\'', '`'), + ('\\', '\''), + (']', ';'), + ('^', '&'), + ('`', '<'), + ('{', 'Ñ'), + ('|', '"'), + ('}', '`'), + ('~', '>'), + ], + "com.apple.keylayout.Swedish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Swedish-Pro" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwedishSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '@'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwissFrench" => &[ + ('!', '+'), + ('"', '`'), + ('#', '*'), + ('$', 'ç'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', '!'), + ('/', '\''), + (':', 'ü'), + (';', 'è'), + ('<', ';'), + ('=', '¨'), + ('>', ':'), + ('@', '"'), + ('[', 'é'), + ('\'', '^'), + ('\\', '$'), + (']', 'à'), + ('^', '&'), + ('`', '<'), + ('{', 'ö'), + ('|', '£'), + ('}', 'ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwissGerman" => &[ + ('!', '+'), + ('"', '`'), + ('#', '*'), + ('$', 'ç'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', '!'), + ('/', '\''), + (':', 'è'), + (';', 'ü'), + ('<', ';'), + ('=', '¨'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '^'), + ('\\', '$'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'é'), + ('|', '£'), + ('}', 'à'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish" => &[ + ('"', '-'), + ('#', '"'), + ('$', '\''), + ('%', '('), + ('&', ')'), + ('(', '%'), + (')', ':'), + ('*', '_'), + (',', 'ö'), + ('-', 'ş'), + ('.', 'ç'), + ('/', '.'), + (':', '$'), + ('<', 'Ö'), + ('>', 'Ç'), + ('@', '*'), + ('[', 'ğ'), + ('\'', ','), + ('\\', 'ü'), + (']', 'ı'), + ('^', '/'), + ('_', 'Ş'), + ('`', '<'), + ('{', 'Ğ'), + ('|', 'Ü'), + ('}', 'I'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish-QWERTY-PC" => &[ + ('"', 'I'), + ('#', '^'), + ('$', '+'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', ':'), + (',', 'ö'), + ('.', 'ç'), + ('/', '*'), + (':', 'Ş'), + (';', 'ş'), + ('<', 'Ö'), + ('=', '.'), + ('>', 'Ç'), + ('@', '\''), + ('[', 'ğ'), + ('\'', 'ı'), + ('\\', ','), + (']', 'ü'), + ('^', '&'), + ('`', '<'), + ('{', 'Ğ'), + ('|', ';'), + ('}', 'Ü'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish-Standard" => &[ + ('"', 'Ş'), + ('#', '^'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (',', '.'), + ('.', ','), + (':', 'Ç'), + (';', 'ç'), + ('<', ':'), + ('=', '*'), + ('>', ';'), + ('@', '"'), + ('[', 'ğ'), + ('\'', 'ş'), + ('\\', 'ü'), + (']', 'ı'), + ('^', '&'), + ('`', 'ö'), + ('{', 'Ğ'), + ('|', 'Ü'), + ('}', 'I'), + ('~', 'Ö'), + ], + "com.apple.keylayout.Turkmen" => &[ + ('C', 'Ç'), + ('Q', 'Ä'), + ('V', 'Ý'), + ('X', 'Ü'), + ('[', 'ň'), + ('\\', 'ş'), + (']', 'ö'), + ('^', '№'), + ('`', 'ž'), + ('c', 'ç'), + ('q', 'ä'), + ('v', 'ý'), + ('x', 'ü'), + ('{', 'Ň'), + ('|', 'Ş'), + ('}', 'Ö'), + ('~', 'Ž'), + ], + "com.apple.keylayout.USInternational-PC" => &[('^', 'ˆ'), ('~', '˜')], + "com.apple.keylayout.Welsh" => &[('#', '£')], - Some(HashMap::from_iter(from.chars().zip(to.chars()))) + _ => return None, + }; + + Some(HashMap::from_iter(mappings.into_iter().cloned())) } #[cfg(not(target_os = "macos"))] diff --git a/crates/snippet/src/snippet.rs b/crates/snippet/src/snippet.rs index 41529939a1..3eeaff285e 100644 --- a/crates/snippet/src/snippet.rs +++ b/crates/snippet/src/snippet.rs @@ -8,7 +8,11 @@ pub struct Snippet { pub tabstops: Vec, } -type TabStop = SmallVec<[Range; 2]>; +#[derive(Clone, Debug, Default, PartialEq)] +pub struct TabStop { + pub ranges: SmallVec<[Range; 2]>, + pub choices: Option>, +} impl Snippet { pub fn parse(source: &str) -> Result { @@ -24,7 +28,11 @@ impl Snippet { if let Some(final_tabstop) = final_tabstop { tabstops.push(final_tabstop); } else { - let end_tabstop = [len..len].into_iter().collect(); + let end_tabstop = TabStop { + ranges: [len..len].into_iter().collect(), + choices: None, + }; + if !tabstops.last().map_or(false, |t| *t == end_tabstop) { tabstops.push(end_tabstop); } @@ -88,11 +96,17 @@ fn parse_tabstop<'a>( ) -> Result<&'a str> { let tabstop_start = text.len(); let tabstop_index; + let mut choices = None; + if source.starts_with('{') { let (index, rest) = parse_int(&source[1..])?; tabstop_index = index; source = rest; + if source.starts_with("|") { + (source, choices) = parse_choices(&source[1..], text)?; + } + if source.starts_with(':') { source = parse_snippet(&source[1..], true, text, tabstops)?; } @@ -110,7 +124,11 @@ fn parse_tabstop<'a>( tabstops .entry(tabstop_index) - .or_default() + .or_insert_with(|| TabStop { + ranges: Default::default(), + choices, + }) + .ranges .push(tabstop_start as isize..text.len() as isize); Ok(source) } @@ -126,6 +144,61 @@ fn parse_int(source: &str) -> Result<(usize, &str)> { Ok((prefix.parse()?, suffix)) } +fn parse_choices<'a>( + mut source: &'a str, + text: &mut String, +) -> Result<(&'a str, Option>)> { + let mut found_default_choice = false; + let mut current_choice = String::new(); + let mut choices = Vec::new(); + + loop { + match source.chars().next() { + None => return Ok(("", Some(choices))), + Some('\\') => { + source = &source[1..]; + + if let Some(c) = source.chars().next() { + if !found_default_choice { + current_choice.push(c); + text.push(c); + } + source = &source[c.len_utf8()..]; + } + } + Some(',') => { + found_default_choice = true; + source = &source[1..]; + choices.push(current_choice); + current_choice = String::new(); + } + Some('|') => { + source = &source[1..]; + choices.push(current_choice); + return Ok((source, Some(choices))); + } + Some(_) => { + let chunk_end = source.find([',', '|', '\\']); + + if chunk_end.is_none() { + return Err(anyhow!( + "Placeholder choice doesn't contain closing pipe-character '|'" + )); + } + + let (chunk, rest) = source.split_at(chunk_end.unwrap()); + + if !found_default_choice { + text.push_str(chunk); + } + + current_choice.push_str(chunk); + source = rest; + } + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -142,11 +215,13 @@ mod tests { let snippet = Snippet::parse("one$1two").unwrap(); assert_eq!(snippet.text, "onetwo"); assert_eq!(tabstops(&snippet), &[vec![3..3], vec![6..6]]); + assert_eq!(tabstop_choices(&snippet), &[&None, &None]); // Multi-digit numbers let snippet = Snippet::parse("one$123-$99-two").unwrap(); assert_eq!(snippet.text, "one--two"); assert_eq!(tabstops(&snippet), &[vec![4..4], vec![3..3], vec![8..8]]); + assert_eq!(tabstop_choices(&snippet), &[&None, &None, &None]); } #[test] @@ -157,6 +232,7 @@ mod tests { // an additional tabstop at the end. assert_eq!(snippet.text, r#"foo."#); assert_eq!(tabstops(&snippet), &[vec![4..4]]); + assert_eq!(tabstop_choices(&snippet), &[&None]); } #[test] @@ -167,6 +243,7 @@ mod tests { // don't insert an additional tabstop at the end. assert_eq!(snippet.text, r#"
"#); assert_eq!(tabstops(&snippet), &[vec![12..12], vec![14..14]]); + assert_eq!(tabstop_choices(&snippet), &[&None, &None]); } #[test] @@ -177,6 +254,30 @@ mod tests { tabstops(&snippet), &[vec![3..6], vec![11..15], vec![15..15]] ); + assert_eq!(tabstop_choices(&snippet), &[&None, &None, &None]); + } + + #[test] + fn test_snippet_with_choice_placeholders() { + let snippet = Snippet::parse("type ${1|i32, u32|} = $2") + .expect("Should be able to unpack choice placeholders"); + + assert_eq!(snippet.text, "type i32 = "); + assert_eq!(tabstops(&snippet), &[vec![5..8], vec![11..11],]); + assert_eq!( + tabstop_choices(&snippet), + &[&Some(vec!["i32".to_string(), " u32".to_string()]), &None] + ); + + let snippet = Snippet::parse(r"${1|\$\{1\|one\,two\,tree\|\}|}") + .expect("Should be able to parse choice with escape characters"); + + assert_eq!(snippet.text, "${1|one,two,tree|}"); + assert_eq!(tabstops(&snippet), &[vec![0..18], vec![18..18]]); + assert_eq!( + tabstop_choices(&snippet), + &[&Some(vec!["${1|one,two,tree|}".to_string(),]), &None] + ); } #[test] @@ -196,6 +297,10 @@ mod tests { vec![40..40], ] ); + assert_eq!( + tabstop_choices(&snippet), + &[&None, &None, &None, &None, &None] + ); } #[test] @@ -203,10 +308,12 @@ mod tests { let snippet = Snippet::parse("\"\\$schema\": $1").unwrap(); assert_eq!(snippet.text, "\"$schema\": "); assert_eq!(tabstops(&snippet), &[vec![11..11]]); + assert_eq!(tabstop_choices(&snippet), &[&None]); let snippet = Snippet::parse("{a\\}").unwrap(); assert_eq!(snippet.text, "{a}"); assert_eq!(tabstops(&snippet), &[vec![3..3]]); + assert_eq!(tabstop_choices(&snippet), &[&None]); // backslash not functioning as an escape let snippet = Snippet::parse("a\\b").unwrap(); @@ -221,6 +328,10 @@ mod tests { } fn tabstops(snippet: &Snippet) -> Vec>> { - snippet.tabstops.iter().map(|t| t.to_vec()).collect() + snippet.tabstops.iter().map(|t| t.ranges.to_vec()).collect() + } + + fn tabstop_choices(snippet: &Snippet) -> Vec<&Option>> { + snippet.tabstops.iter().map(|t| &t.choices).collect() } } diff --git a/crates/snippet_provider/Cargo.toml b/crates/snippet_provider/Cargo.toml index 95ab19ebb6..aa4e1a5f84 100644 --- a/crates/snippet_provider/Cargo.toml +++ b/crates/snippet_provider/Cargo.toml @@ -11,6 +11,7 @@ workspace = true [dependencies] anyhow.workspace = true collections.workspace = true +extension.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true diff --git a/crates/snippet_provider/src/extension_snippet.rs b/crates/snippet_provider/src/extension_snippet.rs new file mode 100644 index 0000000000..41a7c886e1 --- /dev/null +++ b/crates/snippet_provider/src/extension_snippet.rs @@ -0,0 +1,26 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::Result; +use extension::{ExtensionHostProxy, ExtensionSnippetProxy}; +use gpui::AppContext; + +use crate::SnippetRegistry; + +pub fn init(cx: &mut AppContext) { + let proxy = ExtensionHostProxy::default_global(cx); + proxy.register_snippet_proxy(SnippetRegistryProxy { + snippet_registry: SnippetRegistry::global(cx), + }); +} + +struct SnippetRegistryProxy { + snippet_registry: Arc, +} + +impl ExtensionSnippetProxy for SnippetRegistryProxy { + fn register_snippet(&self, path: &PathBuf, snippet_contents: &str) -> Result<()> { + self.snippet_registry + .register_snippets(path, snippet_contents) + } +} diff --git a/crates/snippet_provider/src/lib.rs b/crates/snippet_provider/src/lib.rs index 17d60d25a0..34aa1ebefc 100644 --- a/crates/snippet_provider/src/lib.rs +++ b/crates/snippet_provider/src/lib.rs @@ -1,3 +1,4 @@ +mod extension_snippet; mod format; mod registry; @@ -18,6 +19,7 @@ use util::ResultExt; pub fn init(cx: &mut AppContext) { SnippetRegistry::init_global(cx); + extension_snippet::init(cx); } // Is `None` if the snippet file is global. diff --git a/crates/supermaven/Cargo.toml b/crates/supermaven/Cargo.toml index e04d0ef51b..5af03b1b1b 100644 --- a/crates/supermaven/Cargo.toml +++ b/crates/supermaven/Cargo.toml @@ -16,25 +16,22 @@ doctest = false anyhow.workspace = true client.workspace = true collections.workspace = true -editor.workspace = true -gpui.workspace = true futures.workspace = true +gpui.workspace = true +inline_completion.workspace = true language.workspace = true log.workspace = true postage.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true -supermaven_api.workspace = true smol.workspace = true +supermaven_api.workspace = true text.workspace = true ui.workspace = true unicode-segmentation.workspace = true util.workspace = true -[target.'cfg(target_os = "windows")'.dependencies] -windows.workspace = true - [dev-dependencies] editor = { workspace = true, features = ["test-support"] } env_logger.workspace = true diff --git a/crates/supermaven/src/supermaven.rs b/crates/supermaven/src/supermaven.rs index 152a41c3be..c39bef557e 100644 --- a/crates/supermaven/src/supermaven.rs +++ b/crates/supermaven/src/supermaven.rs @@ -21,7 +21,7 @@ use serde::{Deserialize, Serialize}; use settings::SettingsStore; use smol::{ io::AsyncWriteExt, - process::{Child, ChildStdin, ChildStdout, Command}, + process::{Child, ChildStdin, ChildStdout}, }; use std::{path::PathBuf, process::Stdio, sync::Arc}; use ui::prelude::*; @@ -269,21 +269,14 @@ impl SupermavenAgent { client: Arc, cx: &mut ModelContext, ) -> Result { - let mut process = Command::new(&binary_path); - process + let mut process = util::command::new_smol_command(&binary_path) .arg("stdio") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) - .kill_on_drop(true); - - #[cfg(target_os = "windows")] - { - use smol::process::windows::CommandExt; - process.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); - } - - let mut process = process.spawn().context("failed to start the binary")?; + .kill_on_drop(true) + .spawn() + .context("failed to start the binary")?; let stdin = process .stdin diff --git a/crates/supermaven/src/supermaven_completion_provider.rs b/crates/supermaven/src/supermaven_completion_provider.rs index b9185c9762..5e77cc21ef 100644 --- a/crates/supermaven/src/supermaven_completion_provider.rs +++ b/crates/supermaven/src/supermaven_completion_provider.rs @@ -1,9 +1,9 @@ use crate::{Supermaven, SupermavenCompletionStateId}; use anyhow::Result; use client::telemetry::Telemetry; -use editor::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider}; use futures::StreamExt as _; use gpui::{AppContext, EntityId, Model, ModelContext, Task}; +use inline_completion::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider}; use language::{language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot}; use std::{ ops::{AddAssign, Range}, diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index c81a8b1763..f2fcf50044 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -60,6 +60,10 @@ pub struct SpawnInTerminal { pub shell: Shell, /// Tells debug tasks which program to debug pub program: Option, + /// Whether to show the task summary line in the task output (sucess/failure). + pub show_summary: bool, + /// Whether to show the command line in the task output. + pub show_command: bool, } /// A final form of the [`TaskTemplate`], that got resolved with a particualar [`TaskContext`] and now is ready to spawn the actual task. diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 6e5f67c244..3dcf5b9add 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -1,4 +1,5 @@ use std::path::PathBuf; +use util::serde::default_true; use anyhow::{bail, Context}; use collections::{HashMap, HashSet}; @@ -60,6 +61,12 @@ pub struct TaskTemplate { /// Which shell to use when spawning the task. #[serde(default)] pub shell: Shell, + /// Whether to show the task line in the task output. + #[serde(default = "default_true")] + pub show_summary: bool, + /// Whether to show the command line in the task output. + #[serde(default = "default_true")] + pub show_command: bool, } /// Represents the type of task that is being ran @@ -313,6 +320,8 @@ impl TaskTemplate { hide: self.hide, shell: self.shell.clone(), program, + show_summary: self.show_summary, + show_command: self.show_command, }), }) } diff --git a/crates/tasks_ui/Cargo.toml b/crates/tasks_ui/Cargo.toml index 265755319b..528d238329 100644 --- a/crates/tasks_ui/Cargo.toml +++ b/crates/tasks_ui/Cargo.toml @@ -25,7 +25,7 @@ ui.workspace = true util.workspace = true workspace.workspace = true language.workspace = true - +zed_actions.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } diff --git a/crates/tasks_ui/src/lib.rs b/crates/tasks_ui/src/lib.rs index 133f27eb40..fc4a39a01f 100644 --- a/crates/tasks_ui/src/lib.rs +++ b/crates/tasks_ui/src/lib.rs @@ -3,6 +3,7 @@ use editor::{tasks::task_context, Editor}; use gpui::{AppContext, Task as AsyncTask, ViewContext, WindowContext}; use modal::TasksModal; use project::{Location, WorktreeId}; +use task::TaskId; use task::TaskModal; use workspace::tasks::schedule_task; use workspace::{tasks::schedule_resolved_task, Workspace}; @@ -26,9 +27,13 @@ pub fn init(cx: &mut AppContext) { .read(cx) .task_inventory() .and_then(|inventory| { - inventory - .read(cx) - .last_scheduled_task(action.task_id.as_ref()) + inventory.read(cx).last_scheduled_task( + action + .task_id + .as_ref() + .map(|id| TaskId(id.clone())) + .as_ref(), + ) }) { if action.reevaluate_context { diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index d1ffac9397..f9b6f9a350 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -3,13 +3,13 @@ use std::sync::Arc; use crate::active_item_selection_properties; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - impl_actions, rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusableView, + rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusableView, InteractiveElement, Model, ParentElement, Render, SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, }; use picker::{highlighted_match_with_paths::HighlightedText, Picker, PickerDelegate}; use project::{task_store::TaskStore, TaskSourceKind}; -use task::{ResolvedTask, TaskContext, TaskId, TaskModal, TaskTemplate, TaskType}; +use task::{ResolvedTask, TaskContext, TaskModal, TaskTemplate, TaskType}; use ui::{ div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color, FluentBuilder as _, Icon, IconButton, IconButtonShape, IconName, IconSize, IntoElement, @@ -18,48 +18,7 @@ use ui::{ }; use util::ResultExt; use workspace::{tasks::schedule_resolved_task, ModalView, Workspace}; - -use serde::Deserialize; - -/// Spawn a task with name or open tasks modal -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct Spawn { - #[serde(default)] - /// Name of the task to spawn. - /// If it is not set, a modal with a list of available tasks is opened instead. - /// Defaults to None. - pub task_name: Option, -} - -impl Spawn { - pub fn modal() -> Self { - Self { task_name: None } - } -} - -/// Rerun last task -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct Rerun { - /// Controls whether the task context is reevaluated prior to execution of a task. - /// If it is not, environment variables such as ZED_COLUMN, ZED_FILE are gonna be the same as in the last execution of a task - /// If it is, these variables will be updated to reflect current state of editor at the time task::Rerun is executed. - /// default: false - #[serde(default)] - pub reevaluate_context: bool, - /// Overrides `allow_concurrent_runs` property of the task being reran. - /// Default: null - #[serde(default)] - pub allow_concurrent_runs: Option, - /// Overrides `use_new_terminal` property of the task being reran. - /// Default: null - #[serde(default)] - pub use_new_terminal: Option, - - /// If present, rerun the task with this ID, otherwise rerun the last task. - pub task_id: Option, -} - -impl_actions!(task, [Rerun, Spawn]); +pub use zed_actions::{Rerun, Spawn}; /// A modal used to spawn new tasks. pub(crate) struct TasksModalDelegate { diff --git a/crates/terminal/src/mappings/keys.rs b/crates/terminal/src/mappings/keys.rs index 2d4fe4c62e..1efc1f17d2 100644 --- a/crates/terminal/src/mappings/keys.rs +++ b/crates/terminal/src/mappings/keys.rs @@ -343,7 +343,7 @@ mod test { function: false, }, key: "🖖🏻".to_string(), //2 char string - ime_key: None, + key_char: None, }; assert_eq!(to_esc_str(&ks, &TermMode::NONE, false), None); } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index df5b6284d7..3ffbea5719 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -642,6 +642,8 @@ pub struct TaskState { pub status: TaskStatus, pub completion_rx: Receiver<()>, pub hide: HideStrategy, + pub show_summary: bool, + pub show_command: bool, } /// A status of the current terminal tab's task. @@ -1767,11 +1769,22 @@ impl Terminal { }; let (finished_successfully, task_line, command_line) = task_summary(task, error_code); - // SAFETY: the invocation happens on non `TaskStatus::Running` tasks, once, - // after either `AlacTermEvent::Exit` or `AlacTermEvent::ChildExit` events that are spawned - // when Zed task finishes and no more output is made. - // After the task summary is output once, no more text is appended to the terminal. - unsafe { append_text_to_term(&mut self.term.lock(), &[&task_line, &command_line]) }; + let mut lines_to_show = Vec::new(); + if task.show_summary { + lines_to_show.push(task_line.as_str()); + } + if task.show_command { + lines_to_show.push(command_line.as_str()); + } + + if !lines_to_show.is_empty() { + // SAFETY: the invocation happens on non `TaskStatus::Running` tasks, once, + // after either `AlacTermEvent::Exit` or `AlacTermEvent::ChildExit` events that are spawned + // when Zed task finishes and no more output is made. + // After the task summary is output once, no more text is appended to the terminal. + unsafe { append_text_to_term(&mut self.term.lock(), &lines_to_show) }; + } + match task.hide { HideStrategy::Never => {} HideStrategy::Always => { diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index e48e23b141..842f00ad9f 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -21,7 +21,7 @@ pub enum TerminalDockPosition { #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct Toolbar { - pub title: bool, + pub breadcrumbs: bool, } #[derive(Debug, Deserialize)] @@ -286,10 +286,14 @@ pub enum WorkingDirectory { // Toolbar related settings #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct ToolbarContent { - /// Whether to display the terminal title in its toolbar. + /// Whether to display the terminal title in breadcrumbs inside the terminal pane. + /// Only shown if the terminal title is not empty. + /// + /// The shell running in the terminal needs to be configured to emit the title. + /// Example: `echo -e "\e]2;New Title\007";` /// /// Default: true - pub title: Option, + pub breadcrumbs: Option, } #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index 09b0b0d2d5..e57d9d1fc6 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -14,8 +14,9 @@ doctest = false [dependencies] anyhow.workspace = true -db.workspace = true +breadcrumbs.workspace = true collections.workspace = true +db.workspace = true dirs.workspace = true editor.workspace = true futures.workspace = true @@ -24,7 +25,6 @@ itertools.workspace = true language.workspace = true project.workspace = true task.workspace = true -tasks_ui.workspace = true search.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index bc4f58a5ef..9d5eb7d410 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1001,6 +1001,7 @@ impl InputHandler for TerminalInputHandler { fn text_for_range( &mut self, _: std::ops::Range, + _: &mut Option>, _: &mut WindowContext, ) -> Option { None diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index c3ab20a411..74077974f3 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -1,6 +1,7 @@ use std::{ops::ControlFlow, path::PathBuf, sync::Arc}; use crate::{default_working_directory, TerminalView}; +use breadcrumbs::Breadcrumbs; use collections::{HashMap, HashSet}; use db::kvp::KEY_VALUE_STORE; use futures::future::join_all; @@ -138,8 +139,11 @@ impl TerminalPanel { ControlFlow::Break(()) }); let buffer_search_bar = cx.new_view(search::BufferSearchBar::new); - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx)); + let breadcrumbs = cx.new_view(|_| Breadcrumbs::new()); + pane.toolbar().update(cx, |toolbar, cx| { + toolbar.add_item(buffer_search_bar, cx); + toolbar.add_item(breadcrumbs, cx); + }); pane }); let subscriptions = vec![ @@ -218,7 +222,7 @@ impl TerminalPanel { // context menu will be gone the moment we spawn the modal. .action( "Spawn task", - tasks_ui::Spawn::modal().boxed_clone(), + zed_actions::Spawn::modal().boxed_clone(), ) }); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 6a23e45f54..ad0c7f520d 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -109,7 +109,7 @@ pub struct TerminalView { blink_epoch: usize, can_navigate_to_selected_word: bool, workspace_id: Option, - show_title: bool, + show_breadcrumbs: bool, block_below_cursor: Option>, scroll_top: Pixels, _subscriptions: Vec, @@ -189,7 +189,7 @@ impl TerminalView { blink_epoch: 0, can_navigate_to_selected_word: false, workspace_id, - show_title: TerminalSettings::get_global(cx).toolbar.title, + show_breadcrumbs: TerminalSettings::get_global(cx).toolbar.breadcrumbs, block_below_cursor: None, scroll_top: Pixels::ZERO, _subscriptions: vec![ @@ -259,7 +259,7 @@ impl TerminalView { fn settings_changed(&mut self, cx: &mut ViewContext) { let settings = TerminalSettings::get_global(cx); - self.show_title = settings.toolbar.title; + self.show_breadcrumbs = settings.toolbar.breadcrumbs; let new_cursor_shape = settings.cursor_shape.unwrap_or_default(); let old_cursor_shape = self.cursor_shape; @@ -1044,8 +1044,8 @@ impl Item for TerminalView { .shape(ui::IconButtonShape::Square) .tooltip(|cx| Tooltip::text("Rerun task", cx)) .on_click(move |_, cx| { - cx.dispatch_action(Box::new(tasks_ui::Rerun { - task_id: Some(task_id.clone()), + cx.dispatch_action(Box::new(zed_actions::Rerun { + task_id: Some(task_id.0.clone()), allow_concurrent_runs: Some(true), use_new_terminal: Some(false), reevaluate_context: false, @@ -1145,8 +1145,8 @@ impl Item for TerminalView { Some(Box::new(handle.clone())) } - fn breadcrumb_location(&self) -> ToolbarItemLocation { - if self.show_title { + fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation { + if self.show_breadcrumbs && !self.terminal().read(cx).breadcrumb_text.trim().is_empty() { ToolbarItemLocation::PrimaryLeft } else { ToolbarItemLocation::Hidden diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index e0d4fb4244..cf860ad452 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -315,6 +315,22 @@ impl Theme { pub fn window_background_appearance(&self) -> WindowBackgroundAppearance { self.styles.window_background_appearance } + + /// Darkens the color by reducing its lightness. + /// The resulting lightness is clamped to ensure it doesn't go below 0.0. + /// + /// The first value darkens light appearance mode, the second darkens appearance dark mode. + /// + /// Note: This is a tentative solution and may be replaced with a more robust color system. + pub fn darken(&self, color: Hsla, light_amount: f32, dark_amount: f32) -> Hsla { + let amount = match self.appearance { + Appearance::Light => light_amount, + Appearance::Dark => dark_amount, + }; + let mut hsla = color; + hsla.l = (hsla.l - amount).max(0.0); + hsla + } } /// Compounds a color with an alpha value. diff --git a/crates/theme_extension/Cargo.toml b/crates/theme_extension/Cargo.toml new file mode 100644 index 0000000000..1e12f037b9 --- /dev/null +++ b/crates/theme_extension/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "theme_extension" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/theme_extension.rs" + +[dependencies] +anyhow.workspace = true +extension.workspace = true +fs.workspace = true +gpui.workspace = true +theme.workspace = true diff --git a/crates/theme_extension/LICENSE-GPL b/crates/theme_extension/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/theme_extension/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/theme_extension/src/theme_extension.rs b/crates/theme_extension/src/theme_extension.rs new file mode 100644 index 0000000000..0266db324b --- /dev/null +++ b/crates/theme_extension/src/theme_extension.rs @@ -0,0 +1,47 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::Result; +use extension::{ExtensionHostProxy, ExtensionThemeProxy}; +use fs::Fs; +use gpui::{AppContext, BackgroundExecutor, SharedString, Task}; +use theme::{ThemeRegistry, ThemeSettings}; + +pub fn init( + extension_host_proxy: Arc, + theme_registry: Arc, + executor: BackgroundExecutor, +) { + extension_host_proxy.register_theme_proxy(ThemeRegistryProxy { + theme_registry, + executor, + }); +} + +struct ThemeRegistryProxy { + theme_registry: Arc, + executor: BackgroundExecutor, +} + +impl ExtensionThemeProxy for ThemeRegistryProxy { + fn list_theme_names(&self, theme_path: PathBuf, fs: Arc) -> Task>> { + self.executor.spawn(async move { + let themes = theme::read_user_theme(&theme_path, fs).await?; + Ok(themes.themes.into_iter().map(|theme| theme.name).collect()) + }) + } + + fn remove_user_themes(&self, themes: Vec) { + self.theme_registry.remove_user_themes(&themes); + } + + fn load_user_theme(&self, theme_path: PathBuf, fs: Arc) -> Task> { + let theme_registry = self.theme_registry.clone(); + self.executor + .spawn(async move { theme_registry.load_user_theme(&theme_path, fs).await }) + } + + fn reload_current_theme(&self, cx: &mut AppContext) { + ThemeSettings::reload_current_theme(cx) + } +} diff --git a/crates/theme_selector/Cargo.toml b/crates/theme_selector/Cargo.toml index ec7e9aa877..dc0d5f3ac4 100644 --- a/crates/theme_selector/Cargo.toml +++ b/crates/theme_selector/Cargo.toml @@ -25,5 +25,6 @@ theme.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true +zed_actions.workspace = true [dev-dependencies] diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index d0763c2793..e09ad40bf4 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -2,25 +2,18 @@ use client::telemetry::Telemetry; use fs::Fs; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ - actions, impl_actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, - UpdateGlobal, View, ViewContext, VisualContext, WeakView, + actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, UpdateGlobal, View, + ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; -use serde::Deserialize; use settings::{update_settings_file, SettingsStore}; use std::sync::Arc; use theme::{Appearance, Theme, ThemeMeta, ThemeRegistry, ThemeSettings}; use ui::{prelude::*, v_flex, ListItem, ListItemSpacing}; use util::ResultExt; use workspace::{ui::HighlightedLabel, ModalView, Workspace}; +use zed_actions::theme_selector::Toggle; -#[derive(PartialEq, Clone, Default, Debug, Deserialize)] -pub struct Toggle { - /// A list of theme names to filter the theme selector down to. - pub themes_filter: Option>, -} - -impl_actions!(theme_selector, [Toggle]); actions!(theme_selector, [Reload]); pub fn init(cx: &mut AppContext) { diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index df991613ae..0a2878b357 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -19,7 +19,6 @@ test-support = [ "call/test-support", "client/test-support", "collections/test-support", - "editor/test-support", "gpui/test-support", "http_client/test-support", "project/test-support", @@ -31,24 +30,18 @@ test-support = [ auto_update.workspace = true call.workspace = true client.workspace = true -command_palette.workspace = true -extensions_ui.workspace = true -feedback.workspace = true feature_flags.workspace = true gpui.workspace = true notifications.workspace = true project.workspace = true -recent_projects.workspace = true remote.workspace = true rpc.workspace = true serde.workspace = true smallvec.workspace = true story = { workspace = true, optional = true } theme.workspace = true -theme_selector.workspace = true ui.workspace = true util.workspace = true -vcs_menu.workspace = true workspace.workspace = true zed_actions.workspace = true @@ -59,7 +52,6 @@ windows.workspace = true call = { workspace = true, features = ["test-support"] } client = { workspace = true, features = ["test-support"] } collections = { workspace = true, features = ["test-support"] } -editor = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } http_client = { workspace = true, features = ["test-support"] } notifications = { workspace = true, features = ["test-support"] } diff --git a/crates/title_bar/src/application_menu.rs b/crates/title_bar/src/application_menu.rs index 13ee10c141..ef13655bdb 100644 --- a/crates/title_bar/src/application_menu.rs +++ b/crates/title_bar/src/application_menu.rs @@ -18,7 +18,10 @@ impl Render for ApplicationMenu { .menu(move |cx| { ContextMenu::build(cx, move |menu, cx| { menu.header("Workspace") - .action("Open Command Palette", Box::new(command_palette::Toggle)) + .action( + "Open Command Palette", + Box::new(zed_actions::command_palette::Toggle), + ) .when_some(cx.focused(), |menu, focused| menu.context(focused)) .custom_row(move |cx| { h_flex() @@ -100,7 +103,7 @@ impl Render for ApplicationMenu { .action("Open a new Project...", Box::new(workspace::Open)) .action( "Open Recent Projects...", - Box::new(recent_projects::OpenRecent { + Box::new(zed_actions::OpenRecent { create_new_window: false, }), ) @@ -113,7 +116,10 @@ impl Render for ApplicationMenu { url: "https://zed.dev/docs".into(), }), ) - .action("Give Feedback", Box::new(feedback::GiveFeedback)) + .action( + "Give Feedback", + Box::new(zed_actions::feedback::GiveFeedback), + ) .action("Check for Updates", Box::new(auto_update::Check)) .action("View Telemetry", Box::new(zed_actions::OpenTelemetryLog)) .action( diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index 805c0e7202..649dfb34f7 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -284,9 +284,7 @@ impl TitleBar { let is_connecting_to_project = self .workspace - .update(cx, |workspace, cx| { - recent_projects::is_connecting_over_ssh(workspace, cx) - }) + .update(cx, |workspace, cx| workspace.has_active_modal(cx)) .unwrap_or(false); let room = room.read(cx); diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 2ea9ddafd7..4e9a99433a 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -18,7 +18,6 @@ use gpui::{ StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView, }; use project::{Project, RepositoryEntry}; -use recent_projects::{OpenRemote, RecentProjects}; use rpc::proto; use smallvec::SmallVec; use std::sync::Arc; @@ -28,9 +27,8 @@ use ui::{ IconSize, IconWithIndicator, Indicator, PopoverMenu, Tooltip, }; use util::ResultExt; -use vcs_menu::{BranchList, OpenRecent as ToggleVcsMenu}; use workspace::{notifications::NotifyResultExt, Workspace}; -use zed_actions::OpenBrowser; +use zed_actions::{OpenBrowser, OpenRecent, OpenRemote}; #[cfg(feature = "stories")] pub use stories::*; @@ -397,7 +395,6 @@ impl TitleBar { "Open recent project".to_string() }; - let workspace = self.workspace.clone(); Button::new("project_name_trigger", name) .when(!is_project_selected, |b| b.color(Color::Muted)) .style(ButtonStyle::Subtle) @@ -405,18 +402,19 @@ impl TitleBar { .tooltip(move |cx| { Tooltip::for_action( "Recent Projects", - &recent_projects::OpenRecent { + &zed_actions::OpenRecent { create_new_window: false, }, cx, ) }) .on_click(cx.listener(move |_, _, cx| { - if let Some(workspace) = workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - RecentProjects::open(workspace, false, cx); - }) - } + cx.dispatch_action( + OpenRecent { + create_new_window: false, + } + .boxed_clone(), + ); })) } @@ -443,14 +441,14 @@ impl TitleBar { .tooltip(move |cx| { Tooltip::with_meta( "Recent Branches", - Some(&ToggleVcsMenu), + Some(&zed_actions::branches::OpenRecent), "Local branches only", cx, ) }) .on_click(move |_, cx| { - let _ = workspace.update(cx, |this, cx| { - BranchList::open(this, &Default::default(), cx); + let _ = workspace.update(cx, |_this, cx| { + cx.dispatch_action(zed_actions::branches::OpenRecent.boxed_clone()); }); }), ) @@ -580,8 +578,11 @@ impl TitleBar { }) .action("Settings", zed_actions::OpenSettings.boxed_clone()) .action("Key Bindings", Box::new(zed_actions::OpenKeymap)) - .action("Themes…", theme_selector::Toggle::default().boxed_clone()) - .action("Extensions", extensions_ui::Extensions.boxed_clone()) + .action( + "Themes…", + zed_actions::theme_selector::Toggle::default().boxed_clone(), + ) + .action("Extensions", zed_actions::Extensions.boxed_clone()) .separator() .link( "Book Onboarding", @@ -616,8 +617,11 @@ impl TitleBar { ContextMenu::build(cx, |menu, _| { menu.action("Settings", zed_actions::OpenSettings.boxed_clone()) .action("Key Bindings", Box::new(zed_actions::OpenKeymap)) - .action("Themes…", theme_selector::Toggle::default().boxed_clone()) - .action("Extensions", extensions_ui::Extensions.boxed_clone()) + .action( + "Themes…", + zed_actions::theme_selector::Toggle::default().boxed_clone(), + ) + .action("Extensions", zed_actions::Extensions.boxed_clone()) .separator() .link( "Book Onboarding", diff --git a/crates/toolchain_selector/src/active_toolchain.rs b/crates/toolchain_selector/src/active_toolchain.rs index e2d0b2c808..c49deed02c 100644 --- a/crates/toolchain_selector/src/active_toolchain.rs +++ b/crates/toolchain_selector/src/active_toolchain.rs @@ -4,14 +4,15 @@ use gpui::{ ViewContext, WeakModel, WeakView, }; use language::{Buffer, BufferEvent, LanguageName, Toolchain}; -use project::WorktreeId; -use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, Tooltip}; +use project::{Project, WorktreeId}; +use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, SharedString, Tooltip}; use workspace::{item::ItemHandle, StatusItemView, Workspace}; use crate::ToolchainSelector; pub struct ActiveToolchain { active_toolchain: Option, + term: SharedString, workspace: WeakView, active_buffer: Option<(WorktreeId, WeakModel, Subscription)>, _update_toolchain_task: Task>, @@ -22,6 +23,7 @@ impl ActiveToolchain { Self { active_toolchain: None, active_buffer: None, + term: SharedString::new_static("Toolchain"), workspace: workspace.weak_handle(), _update_toolchain_task: Self::spawn_tracker_task(cx), @@ -44,7 +46,17 @@ impl ActiveToolchain { .update(&mut cx, |this, _| Some(this.language()?.name())) .ok() .flatten()?; - + let term = workspace + .update(&mut cx, |workspace, cx| { + let languages = workspace.project().read(cx).languages(); + Project::toolchain_term(languages.clone(), language_name.clone()) + }) + .ok()? + .await?; + let _ = this.update(&mut cx, |this, cx| { + this.term = term; + cx.notify(); + }); let worktree_id = active_file .update(&mut cx, |this, cx| Some(this.file()?.worktree_id(cx))) .ok() @@ -133,6 +145,7 @@ impl ActiveToolchain { impl Render for ActiveToolchain { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { div().when_some(self.active_toolchain.as_ref(), |el, active_toolchain| { + let term = self.term.clone(); el.child( Button::new("change-toolchain", active_toolchain.name.clone()) .label_size(LabelSize::Small) @@ -143,7 +156,7 @@ impl Render for ActiveToolchain { }); } })) - .tooltip(|cx| Tooltip::text("Select Toolchain", cx)), + .tooltip(move |cx| Tooltip::text(format!("Select {}", &term), cx)), ) }) } diff --git a/crates/toolchain_selector/src/toolchain_selector.rs b/crates/toolchain_selector/src/toolchain_selector.rs index 8a3368f816..4c31d600ba 100644 --- a/crates/toolchain_selector/src/toolchain_selector.rs +++ b/crates/toolchain_selector/src/toolchain_selector.rs @@ -126,6 +126,7 @@ pub struct ToolchainSelectorDelegate { workspace: WeakView, worktree_id: WorktreeId, worktree_abs_path_root: Arc, + placeholder_text: Arc, _fetch_candidates_task: Task>, } @@ -144,6 +145,17 @@ impl ToolchainSelectorDelegate { let _fetch_candidates_task = cx.spawn({ let project = project.clone(); move |this, mut cx| async move { + let term = project + .update(&mut cx, |this, _| { + Project::toolchain_term(this.languages().clone(), language_name.clone()) + }) + .ok()? + .await?; + let placeholder_text = format!("Select a {}…", term.to_lowercase()).into(); + let _ = this.update(&mut cx, move |this, cx| { + this.delegate.placeholder_text = placeholder_text; + this.refresh_placeholder(cx); + }); let available_toolchains = project .update(&mut cx, |this, cx| { this.available_toolchains(worktree_id, language_name, cx) @@ -153,6 +165,7 @@ impl ToolchainSelectorDelegate { let _ = this.update(&mut cx, move |this, cx| { this.delegate.candidates = available_toolchains; + if let Some(active_toolchain) = active_toolchain { if let Some(position) = this .delegate @@ -170,7 +183,7 @@ impl ToolchainSelectorDelegate { Some(()) } }); - + let placeholder_text = "Select a toolchain…".to_string().into(); Self { toolchain_selector: language_selector, candidates: Default::default(), @@ -179,6 +192,7 @@ impl ToolchainSelectorDelegate { workspace, worktree_id, worktree_abs_path_root, + placeholder_text, _fetch_candidates_task, } } @@ -196,7 +210,7 @@ impl PickerDelegate for ToolchainSelectorDelegate { type ListItem = ListItem; fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { - "Select a toolchain...".into() + self.placeholder_text.clone() } fn match_count(&self) -> usize { diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index 32ac1c611f..a7f3618f97 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -202,7 +202,12 @@ impl ButtonStyle { icon_color: Color::Default.color(cx), } } - ButtonStyle::Tinted(tint) => tint.button_like_style(cx), + ButtonStyle::Tinted(tint) => { + let mut styles = tint.button_like_style(cx); + let theme = cx.theme(); + styles.background = theme.darken(styles.background, 0.05, 0.2); + styles + } ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_hover, border_color: transparent_black(), diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index 770e46eafd..328481de6e 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -3,7 +3,7 @@ use crate::PlatformStyle; use crate::{h_flex, prelude::*, Icon, IconName, IconSize}; use gpui::{relative, Action, FocusHandle, IntoElement, Keystroke, WindowContext}; -#[derive(IntoElement, Clone)] +#[derive(Debug, IntoElement, Clone)] pub struct KeyBinding { /// A keybinding consists of a key and a set of modifier keys. /// More then one keybinding produces a chord. diff --git a/crates/ui/src/components/scrollbar.rs b/crates/ui/src/components/scrollbar.rs index c8f09439c3..7dbda4b3fb 100644 --- a/crates/ui/src/components/scrollbar.rs +++ b/crates/ui/src/components/scrollbar.rs @@ -370,7 +370,7 @@ impl Element for Scrollbar { }; if let Some(id) = state.parent_id { - cx.notify(id); + cx.notify(Some(id)); } } } else { @@ -382,7 +382,7 @@ impl Element for Scrollbar { if phase.bubble() { state.drag.take(); if let Some(id) = state.parent_id { - cx.notify(id); + cx.notify(Some(id)); } } }); diff --git a/crates/ui/src/components/tab.rs b/crates/ui/src/components/tab.rs index 1a2c1595cf..e33fc732da 100644 --- a/crates/ui/src/components/tab.rs +++ b/crates/ui/src/components/tab.rs @@ -4,7 +4,7 @@ use std::cmp::Ordering; use gpui::{AnyElement, IntoElement, Stateful}; use smallvec::SmallVec; -use crate::{prelude::*, BASE_REM_SIZE_IN_PX}; +use crate::prelude::*; /// The position of a [`Tab`] within a list of tabs. #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -54,10 +54,6 @@ impl Tab { } } - pub const CONTAINER_HEIGHT_IN_REMS: f32 = 29. / BASE_REM_SIZE_IN_PX; - - const CONTENT_HEIGHT_IN_REMS: f32 = 28. / BASE_REM_SIZE_IN_PX; - pub fn position(mut self, position: TabPosition) -> Self { self.position = position; self @@ -77,6 +73,14 @@ impl Tab { self.end_slot = element.into().map(IntoElement::into_any_element); self } + + pub fn content_height(cx: &mut WindowContext) -> Pixels { + DynamicSpacing::Base32.px(cx) - px(1.) + } + + pub fn container_height(cx: &mut WindowContext) -> Pixels { + DynamicSpacing::Base32.px(cx) + } } impl InteractiveElement for Tab { @@ -130,7 +134,7 @@ impl RenderOnce for Tab { }; self.div - .h(rems(Self::CONTAINER_HEIGHT_IN_REMS)) + .h(Tab::container_height(cx)) .bg(tab_bg) .border_color(cx.theme().colors().border) .map(|this| match self.position { @@ -157,7 +161,7 @@ impl RenderOnce for Tab { h_flex() .group("") .relative() - .h(rems(Self::CONTENT_HEIGHT_IN_REMS)) + .h(Tab::content_height(cx)) .px(DynamicSpacing::Base04.px(cx)) .gap(DynamicSpacing::Base04.rems(cx)) .text_color(text_color) diff --git a/crates/ui/src/components/tab_bar.rs b/crates/ui/src/components/tab_bar.rs index 6aaa2fd59a..7a169f9701 100644 --- a/crates/ui/src/components/tab_bar.rs +++ b/crates/ui/src/components/tab_bar.rs @@ -3,6 +3,7 @@ use gpui::{AnyElement, ScrollHandle}; use smallvec::SmallVec; use crate::prelude::*; +use crate::Tab; #[derive(IntoElement)] pub struct TabBar { @@ -97,11 +98,7 @@ impl RenderOnce for TabBar { .flex() .flex_none() .w_full() - .h( - // TODO: This should scale with [UiDensity], however tabs, - // and other tab bar tools need to scale dynamically first. - rems_from_px(29.), - ) + .h(Tab::container_height(cx)) .bg(cx.theme().colors().tab_bar_background) .when(!self.start_children.is_empty(), |this| { this.child( diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index 58c4686bf9..94d580e643 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -30,6 +30,7 @@ regex.workspace = true rust-embed.workspace = true serde.workspace = true serde_json.workspace = true +smol.workspace = true take-until = "0.2.0" tempfile = { workspace = true, optional = true } unicase.workspace = true diff --git a/crates/util/src/command.rs b/crates/util/src/command.rs new file mode 100644 index 0000000000..85e2234991 --- /dev/null +++ b/crates/util/src/command.rs @@ -0,0 +1,32 @@ +use std::ffi::OsStr; + +#[cfg(target_os = "windows")] +const CREATE_NO_WINDOW: u32 = 0x0800_0000_u32; + +#[cfg(target_os = "windows")] +pub fn new_std_command(program: impl AsRef) -> std::process::Command { + use std::os::windows::process::CommandExt; + + let mut command = std::process::Command::new(program); + command.creation_flags(CREATE_NO_WINDOW); + command +} + +#[cfg(not(target_os = "windows"))] +pub fn new_std_command(program: impl AsRef) -> std::process::Command { + std::process::Command::new(program) +} + +#[cfg(target_os = "windows")] +pub fn new_smol_command(program: impl AsRef) -> smol::process::Command { + use smol::process::windows::CommandExt; + + let mut command = smol::process::Command::new(program); + command.creation_flags(CREATE_NO_WINDOW); + command +} + +#[cfg(not(target_os = "windows"))] +pub fn new_smol_command(program: impl AsRef) -> smol::process::Command { + smol::process::Command::new(program) +} diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index d629c8facc..f4e494f66e 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -378,7 +378,15 @@ pub fn compare_paths( .as_deref() .map(NumericPrefixWithSuffix::from_numeric_prefixed_str); - num_and_remainder_a.cmp(&num_and_remainder_b) + num_and_remainder_a.cmp(&num_and_remainder_b).then_with(|| { + if a_is_file && b_is_file { + let ext_a = path_a.extension().unwrap_or_default(); + let ext_b = path_b.extension().unwrap_or_default(); + ext_a.cmp(ext_b) + } else { + cmp::Ordering::Equal + } + }) }); if !ordering.is_eq() { return ordering; @@ -433,6 +441,28 @@ mod tests { ); } + #[test] + fn compare_paths_with_same_name_different_extensions() { + let mut paths = vec![ + (Path::new("test_dirs/file.rs"), true), + (Path::new("test_dirs/file.txt"), true), + (Path::new("test_dirs/file.md"), true), + (Path::new("test_dirs/file"), true), + (Path::new("test_dirs/file.a"), true), + ]; + paths.sort_by(|&a, &b| compare_paths(a, b)); + assert_eq!( + paths, + vec![ + (Path::new("test_dirs/file"), true), + (Path::new("test_dirs/file.a"), true), + (Path::new("test_dirs/file.md"), true), + (Path::new("test_dirs/file.rs"), true), + (Path::new("test_dirs/file.txt"), true), + ] + ); + } + #[test] fn compare_paths_case_semi_sensitive() { let mut paths = vec![ diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index e27fd65ac7..fe3f7ef9a0 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -1,4 +1,5 @@ pub mod arc_cow; +pub mod command; pub mod fs; pub mod paths; pub mod serde; @@ -148,6 +149,12 @@ pub fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json: } } + (Value::Array(source), Value::Array(target)) => { + for value in source { + target.push(value); + } + } + (source, target) => *target = source, } } diff --git a/crates/vcs_menu/Cargo.toml b/crates/vcs_menu/Cargo.toml index 11de371868..47bf3d8984 100644 --- a/crates/vcs_menu/Cargo.toml +++ b/crates/vcs_menu/Cargo.toml @@ -18,3 +18,4 @@ project.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true +zed_actions.workspace = true diff --git a/crates/vcs_menu/src/lib.rs b/crates/vcs_menu/src/lib.rs index f165c91bfe..f61bad57fa 100644 --- a/crates/vcs_menu/src/lib.rs +++ b/crates/vcs_menu/src/lib.rs @@ -2,10 +2,9 @@ use anyhow::{anyhow, Context, Result}; use fuzzy::{StringMatch, StringMatchCandidate}; use git::repository::Branch; use gpui::{ - actions, rems, AnyElement, AppContext, AsyncAppContext, DismissEvent, EventEmitter, - FocusHandle, FocusableView, InteractiveElement, IntoElement, ParentElement, Render, - SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, - WindowContext, + rems, AnyElement, AppContext, AsyncAppContext, DismissEvent, EventEmitter, FocusHandle, + FocusableView, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, + Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use picker::{Picker, PickerDelegate}; use project::ProjectPath; @@ -14,8 +13,7 @@ use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing}; use util::ResultExt; use workspace::notifications::DetachAndPromptErr; use workspace::{ModalView, Workspace}; - -actions!(branches, [OpenRecent]); +use zed_actions::branches::OpenRecent; pub fn init(cx: &mut AppContext) { cx.observe_new_views(|workspace: &mut Workspace, _| { diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index fddb607c1f..ddf738d067 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -39,6 +39,7 @@ settings.workspace = true tokio = { version = "1.15", features = ["full"], optional = true } ui.workspace = true util.workspace = true +vim_mode_setting.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/vim/src/digraph.rs b/crates/vim/src/digraph.rs index 4c09dd3e33..dcccc8b5cd 100644 --- a/crates/vim/src/digraph.rs +++ b/crates/vim/src/digraph.rs @@ -83,7 +83,7 @@ impl Vim { cx: &mut ViewContext, ) { // handled by handle_literal_input - if keystroke_event.keystroke.ime_key.is_some() { + if keystroke_event.keystroke.key_char.is_some() { return; }; diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 7c1f2fdb4c..f97312e7f8 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -1407,7 +1407,7 @@ mod test { // Generic arguments cx.set_state("fn boop() {}", Mode::Normal); - cx.simulate_keystrokes("v i g"); + cx.simulate_keystrokes("v i a"); cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual); // Function arguments @@ -1415,11 +1415,11 @@ mod test { "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}", Mode::Normal, ); - cx.simulate_keystrokes("d a g"); + cx.simulate_keystrokes("d a a"); cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal); cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal); - cx.simulate_keystrokes("v a g"); + cx.simulate_keystrokes("v a a"); cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual); // Tuple, vec, and array arguments @@ -1427,34 +1427,34 @@ mod test { "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}", Mode::Normal, ); - cx.simulate_keystrokes("c i g"); + cx.simulate_keystrokes("c i a"); cx.assert_state( "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}", Mode::Insert, ); cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal); - cx.simulate_keystrokes("c a g"); + cx.simulate_keystrokes("c a a"); cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert); cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal); - cx.simulate_keystrokes("c i g"); + cx.simulate_keystrokes("c i a"); cx.assert_state("let a = [ˇ, 300];", Mode::Insert); cx.set_state( "let a = vec![Vec::new(), vecˇ![test::call(), 300]];", Mode::Normal, ); - cx.simulate_keystrokes("c a g"); + cx.simulate_keystrokes("c a a"); cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert); // Cursor immediately before / after brackets cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal); - cx.simulate_keystrokes("v i g"); + cx.simulate_keystrokes("v i a"); cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual); cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal); - cx.simulate_keystrokes("v i g"); + cx.simulate_keystrokes("v i a"); cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual); } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 77fc7db9d6..dd3bf297cb 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -41,15 +41,11 @@ use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals}; use std::{mem, ops::Range, sync::Arc}; use surrounds::SurroundsType; use ui::{IntoElement, VisualContext}; +use vim_mode_setting::VimModeSetting; use workspace::{self, Pane, Workspace}; use crate::state::ReplayableAction; -/// Whether or not to enable Vim mode. -/// -/// Default: false -pub struct VimModeSetting(pub bool); - /// An Action to Switch between modes #[derive(Clone, Deserialize, PartialEq)] pub struct SwitchMode(pub Mode); @@ -89,7 +85,7 @@ impl_actions!(vim, [SwitchMode, PushOperator, Number, SelectRegister]); /// Initializes the `vim` crate. pub fn init(cx: &mut AppContext) { - VimModeSetting::register(cx); + vim_mode_setting::init(cx); VimSettings::register(cx); VimGlobals::register(cx); @@ -1122,23 +1118,6 @@ impl Vim { } } -impl Settings for VimModeSetting { - const KEY: Option<&'static str> = Some("vim_mode"); - - type FileContent = Option; - - fn load(sources: SettingsSources, _: &mut AppContext) -> Result { - Ok(Self( - sources - .user - .or(sources.server) - .copied() - .flatten() - .unwrap_or(sources.default.ok_or_else(Self::missing_default)?), - )) - } -} - /// Controls when to use system clipboard. #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] diff --git a/crates/vim_mode_setting/Cargo.toml b/crates/vim_mode_setting/Cargo.toml new file mode 100644 index 0000000000..0c009fdfd6 --- /dev/null +++ b/crates/vim_mode_setting/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "vim_mode_setting" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/vim_mode_setting.rs" + +[dependencies] +anyhow.workspace = true +gpui.workspace = true +settings.workspace = true diff --git a/crates/vim_mode_setting/LICENSE-GPL b/crates/vim_mode_setting/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/vim_mode_setting/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/vim_mode_setting/src/vim_mode_setting.rs b/crates/vim_mode_setting/src/vim_mode_setting.rs new file mode 100644 index 0000000000..072db138df --- /dev/null +++ b/crates/vim_mode_setting/src/vim_mode_setting.rs @@ -0,0 +1,36 @@ +//! Contains the [`VimModeSetting`] used to enable/disable Vim mode. +//! +//! This is in its own crate as we want other crates to be able to enable or +//! disable Vim mode without having to depend on the `vim` crate in its +//! entirety. + +use anyhow::Result; +use gpui::AppContext; +use settings::{Settings, SettingsSources}; + +/// Initializes the `vim_mode_setting` crate. +pub fn init(cx: &mut AppContext) { + VimModeSetting::register(cx); +} + +/// Whether or not to enable Vim mode. +/// +/// Default: false +pub struct VimModeSetting(pub bool); + +impl Settings for VimModeSetting { + const KEY: Option<&'static str> = Some("vim_mode"); + + type FileContent = Option; + + fn load(sources: SettingsSources, _: &mut AppContext) -> Result { + Ok(Self( + sources + .user + .or(sources.server) + .copied() + .flatten() + .unwrap_or(sources.default.ok_or_else(Self::missing_default)?), + )) + } +} diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml index 0db1af9252..473e5e853e 100644 --- a/crates/welcome/Cargo.toml +++ b/crates/welcome/Cargo.toml @@ -17,21 +17,19 @@ test-support = [] [dependencies] anyhow.workspace = true client.workspace = true +copilot.workspace = true db.workspace = true -extensions_ui.workspace = true fuzzy.workspace = true gpui.workspace = true -inline_completion_button.workspace = true install_cli.workspace = true picker.workspace = true project.workspace = true schemars.workspace = true serde.workspace = true settings.workspace = true -theme_selector.workspace = true ui.workspace = true util.workspace = true -vim.workspace = true +vim_mode_setting.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index c8d5bf6dfc..8dcb26bcc1 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -5,14 +5,14 @@ mod multibuffer_hint; use client::{telemetry::Telemetry, TelemetrySettings}; use db::kvp::KEY_VALUE_STORE; use gpui::{ - actions, svg, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement, + actions, svg, Action, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement, ParentElement, Render, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use settings::{Settings, SettingsStore}; use std::sync::Arc; use ui::{prelude::*, CheckboxWithLabel}; -use vim::VimModeSetting; +use vim_mode_setting::VimModeSetting; use workspace::{ dock::DockPosition, item::{Item, ItemEvent}, @@ -73,6 +73,7 @@ impl Render for WelcomePage { h_flex() .size_full() .bg(cx.theme().colors().editor_background) + .key_context("Welcome") .track_focus(&self.focus_handle(cx)) .child( v_flex() @@ -132,12 +133,8 @@ impl Render for WelcomePage { "welcome page: change theme".to_string(), ); this.workspace - .update(cx, |workspace, cx| { - theme_selector::toggle( - workspace, - &Default::default(), - cx, - ) + .update(cx, |_workspace, cx| { + cx.dispatch_action(zed_actions::theme_selector::Toggle::default().boxed_clone()); }) .ok(); })), @@ -177,7 +174,7 @@ impl Render for WelcomePage { this.telemetry.report_app_event( "welcome page: sign in to copilot".to_string(), ); - inline_completion_button::initiate_sign_in(cx); + copilot::initiate_sign_in(cx); }), ), ) @@ -250,7 +247,7 @@ impl Render for WelcomePage { "welcome page: open extensions".to_string(), ); cx.dispatch_action(Box::new( - extensions_ui::Extensions, + zed_actions::Extensions, )); })), ) diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 30ab109879..acc47cd11e 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -15,7 +15,7 @@ use std::sync::Arc; use ui::{h_flex, ContextMenu, IconButton, Tooltip}; use ui::{prelude::*, right_click_menu}; -const RESIZE_HANDLE_SIZE: Pixels = Pixels(6.); +pub(crate) const RESIZE_HANDLE_SIZE: Pixels = Pixels(6.); pub enum PanelEvent { ZoomIn, @@ -574,6 +574,7 @@ impl Dock { pub fn resize_active_panel(&mut self, size: Option, cx: &mut ViewContext) { if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) { let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round()); + entry.panel.set_size(size, cx); cx.notify(); } @@ -593,6 +594,15 @@ impl Dock { dispatch_context } + + pub fn clamp_panel_size(&mut self, max_size: Pixels, cx: &mut WindowContext) { + let max_size = px((max_size.0 - RESIZE_HANDLE_SIZE.0).abs()); + for panel in self.panel_entries.iter().map(|entry| &entry.panel) { + if panel.size(cx) > max_size { + panel.set_size(Some(max_size.max(RESIZE_HANDLE_SIZE)), cx); + } + } + } } impl Render for Dock { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 2f1c900ecf..a7bf90dd17 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -228,6 +228,9 @@ pub trait Item: FocusableView + EventEmitter { fn is_dirty(&self, _: &AppContext) -> bool { false } + fn has_deleted_file(&self, _: &AppContext) -> bool { + false + } fn has_conflict(&self, _: &AppContext) -> bool { false } @@ -275,7 +278,7 @@ pub trait Item: FocusableView + EventEmitter { None } - fn breadcrumb_location(&self) -> ToolbarItemLocation { + fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation { ToolbarItemLocation::Hidden } @@ -405,6 +408,7 @@ pub trait ItemHandle: 'static + Send { fn item_id(&self) -> EntityId; fn to_any(&self) -> AnyView; fn is_dirty(&self, cx: &AppContext) -> bool; + fn has_deleted_file(&self, cx: &AppContext) -> bool; fn has_conflict(&self, cx: &AppContext) -> bool; fn can_save(&self, cx: &AppContext) -> bool; fn save( @@ -768,6 +772,10 @@ impl ItemHandle for View { self.read(cx).is_dirty(cx) } + fn has_deleted_file(&self, cx: &AppContext) -> bool { + self.read(cx).has_deleted_file(cx) + } + fn has_conflict(&self, cx: &AppContext) -> bool { self.read(cx).has_conflict(cx) } @@ -819,7 +827,7 @@ impl ItemHandle for View { } fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation { - self.read(cx).breadcrumb_location() + self.read(cx).breadcrumb_location(cx) } fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option> { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index a6d5bbd947..6dd4a63cc9 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -48,7 +48,7 @@ use ui::{v_flex, ContextMenu}; use util::{debug_panic, maybe, truncate_and_remove_front, ResultExt}; /// A selected entry in e.g. project panel. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct SelectedEntry { pub worktree_id: WorktreeId, pub entry_id: ProjectEntryId, @@ -1548,18 +1548,25 @@ impl Pane { const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?"; + const DELETED_MESSAGE: &str = + "This file has been deleted on disk since you started editing it. Do you want to recreate it?"; + if save_intent == SaveIntent::Skip { return Ok(true); } - let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.update(|cx| { - ( - item.has_conflict(cx), - item.is_dirty(cx), - item.can_save(cx), - item.is_singleton(cx), - ) - })?; + let (mut has_conflict, mut is_dirty, mut can_save, is_singleton, has_deleted_file) = cx + .update(|cx| { + ( + item.has_conflict(cx), + item.is_dirty(cx), + item.can_save(cx), + item.is_singleton(cx), + item.has_deleted_file(cx), + ) + })?; + + let can_save_as = is_singleton; // when saving a single buffer, we ignore whether or not it's dirty. if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat { @@ -1579,22 +1586,45 @@ impl Pane { let should_format = save_intent != SaveIntent::SaveWithoutFormat; if has_conflict && can_save { - let answer = pane.update(cx, |pane, cx| { - pane.activate_item(item_ix, true, true, cx); - cx.prompt( - PromptLevel::Warning, - CONFLICT_MESSAGE, - None, - &["Overwrite", "Discard", "Cancel"], - ) - })?; - match answer.await { - Ok(0) => { - pane.update(cx, |_, cx| item.save(should_format, project, cx))? - .await? + if has_deleted_file && is_singleton { + let answer = pane.update(cx, |pane, cx| { + pane.activate_item(item_ix, true, true, cx); + cx.prompt( + PromptLevel::Warning, + DELETED_MESSAGE, + None, + &["Save", "Close", "Cancel"], + ) + })?; + match answer.await { + Ok(0) => { + pane.update(cx, |_, cx| item.save(should_format, project, cx))? + .await? + } + Ok(1) => { + pane.update(cx, |pane, cx| pane.remove_item(item_ix, false, false, cx))?; + } + _ => return Ok(false), + } + return Ok(true); + } else { + let answer = pane.update(cx, |pane, cx| { + pane.activate_item(item_ix, true, true, cx); + cx.prompt( + PromptLevel::Warning, + CONFLICT_MESSAGE, + None, + &["Overwrite", "Discard", "Cancel"], + ) + })?; + match answer.await { + Ok(0) => { + pane.update(cx, |_, cx| item.save(should_format, project, cx))? + .await? + } + Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?, + _ => return Ok(false), } - Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?, - _ => return Ok(false), } } else if is_dirty && (can_save || can_save_as) { if save_intent == SaveIntent::Close { @@ -2236,7 +2266,6 @@ impl Pane { fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement { let focus_handle = self.focus_handle.clone(); let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft) - .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .on_click({ let view = cx.view().clone(); @@ -2249,7 +2278,6 @@ impl Pane { }); let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight) - .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .on_click({ let view = cx.view().clone(); @@ -2434,6 +2462,8 @@ impl Pane { to_pane = workspace.split_pane(to_pane, split_direction, cx); } let old_ix = from_pane.read(cx).index_for_item_id(item_id); + let old_len = to_pane.read(cx).items.len(); + move_item(&from_pane, &to_pane, item_id, ix, cx); if to_pane == from_pane { if let Some(old_index) = old_ix { to_pane.update(cx, |this, _| { @@ -2451,7 +2481,10 @@ impl Pane { } } else { to_pane.update(cx, |this, _| { - if this.has_pinned_tabs() && ix < this.pinned_tab_count { + if this.items.len() > old_len // Did we not deduplicate on drag? + && this.has_pinned_tabs() + && ix < this.pinned_tab_count + { this.pinned_tab_count += 1; } }); @@ -2463,7 +2496,6 @@ impl Pane { } }) } - move_item(&from_pane, &to_pane, item_id, ix, cx); }); }) .log_err(); diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 6d3daf90d0..10a8230e0a 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -2,6 +2,7 @@ pub mod model; use std::{ path::{Path, PathBuf}, + str::FromStr, sync::Arc, }; @@ -460,6 +461,9 @@ define_connection! { PRIMARY KEY (workspace_id, worktree_id, language_name) ); ), + sql!( + ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}"; + ), sql!( CREATE TABLE breakpoints ( workspace_id INTEGER NOT NULL, @@ -1274,18 +1278,19 @@ impl WorkspaceDb { self.write(move |this| { let mut select = this .select_bound(sql!( - SELECT name, path FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ? + SELECT name, path, raw_json FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ? )) .context("Preparing insertion")?; - let toolchain: Vec<(String, String)> = + let toolchain: Vec<(String, String, String)> = select((workspace_id, language_name.0.to_owned(), worktree_id.to_usize()))?; - Ok(toolchain.into_iter().next().map(|(name, path)| Toolchain { + Ok(toolchain.into_iter().next().and_then(|(name, path, raw_json)| Some(Toolchain { name: name.into(), path: path.into(), language_name, - })) + as_json: serde_json::Value::from_str(&raw_json).ok()? + }))) }) .await } @@ -1297,18 +1302,19 @@ impl WorkspaceDb { self.write(move |this| { let mut select = this .select_bound(sql!( - SELECT name, path, worktree_id, language_name FROM toolchains WHERE workspace_id = ? + SELECT name, path, worktree_id, language_name, raw_json FROM toolchains WHERE workspace_id = ? )) .context("Preparing insertion")?; - let toolchain: Vec<(String, String, u64, String)> = + let toolchain: Vec<(String, String, u64, String, String)> = select(workspace_id)?; - Ok(toolchain.into_iter().map(|(name, path, worktree_id, language_name)| (Toolchain { + Ok(toolchain.into_iter().filter_map(|(name, path, worktree_id, language_name, raw_json)| Some((Toolchain { name: name.into(), path: path.into(), language_name: LanguageName::new(&language_name), - }, WorktreeId::from_proto(worktree_id))).collect()) + as_json: serde_json::Value::from_str(&raw_json).ok()? + }, WorktreeId::from_proto(worktree_id)))).collect()) }) .await } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 5e23938a10..98940f488b 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -21,7 +21,7 @@ use client::{ }; use collections::{hash_map, HashMap, HashSet}; use derive_more::{Deref, DerefMut}; -use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle}; +use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE}; use futures::{ channel::{ mpsc::{self, UnboundedReceiver, UnboundedSender}, @@ -623,7 +623,7 @@ impl AppState { let fs = fs::FakeFs::new(cx.background_executor().clone()); let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); - let clock = Arc::new(clock::FakeSystemClock::default()); + let clock = Arc::new(clock::FakeSystemClock::new()); let http_client = http_client::FakeHttpClient::with_404_response(); let client = Client::new(clock, http_client.clone(), cx); let session = cx.new_model(|cx| AppSession::new(Session::test(), cx)); @@ -1113,10 +1113,17 @@ impl Workspace { ); cx.spawn(|mut cx| async move { - let serialized_workspace: Option = - persistence::DB.workspace_for_roots(abs_paths.as_slice()); + let mut paths_to_open = Vec::with_capacity(abs_paths.len()); + for path in abs_paths.into_iter() { + if let Some(canonical) = app_state.fs.canonicalize(&path).await.ok() { + paths_to_open.push(canonical) + } else { + paths_to_open.push(path) + } + } - let mut paths_to_open = abs_paths; + let serialized_workspace: Option = + persistence::DB.workspace_for_roots(paths_to_open.as_slice()); let workspace_location = serialized_workspace .as_ref() @@ -4858,7 +4865,27 @@ impl Render for Workspace { let this = cx.view().clone(); canvas( move |bounds, cx| { - this.update(cx, |this, _cx| this.bounds = bounds) + this.update(cx, |this, cx| { + let bounds_changed = this.bounds != bounds; + this.bounds = bounds; + + if bounds_changed { + this.left_dock.update(cx, |dock, cx| { + dock.clamp_panel_size(bounds.size.width, cx) + }); + + this.right_dock.update(cx, |dock, cx| { + dock.clamp_panel_size(bounds.size.width, cx) + }); + + this.bottom_dock.update(cx, |dock, cx| { + dock.clamp_panel_size( + bounds.size.height, + cx, + ) + }); + } + }) }, |_, _, _| {}, ) @@ -4870,42 +4897,27 @@ impl Render for Workspace { |workspace, e: &DragMoveEvent, cx| { match e.drag(cx).0 { DockPosition::Left => { - let size = e.event.position.x - - workspace.bounds.left(); - workspace.left_dock.update( + resize_left_dock( + e.event.position.x + - workspace.bounds.left(), + workspace, cx, - |left_dock, cx| { - left_dock.resize_active_panel( - Some(size), - cx, - ); - }, ); } DockPosition::Right => { - let size = workspace.bounds.right() - - e.event.position.x; - workspace.right_dock.update( + resize_right_dock( + workspace.bounds.right() + - e.event.position.x, + workspace, cx, - |right_dock, cx| { - right_dock.resize_active_panel( - Some(size), - cx, - ); - }, ); } DockPosition::Bottom => { - let size = workspace.bounds.bottom() - - e.event.position.y; - workspace.bottom_dock.update( + resize_bottom_dock( + workspace.bounds.bottom() + - e.event.position.y, + workspace, cx, - |bottom_dock, cx| { - bottom_dock.resize_active_panel( - Some(size), - cx, - ); - }, ); } } @@ -4993,6 +5005,40 @@ impl Render for Workspace { } } +fn resize_bottom_dock( + new_size: Pixels, + workspace: &mut Workspace, + cx: &mut ViewContext<'_, Workspace>, +) { + let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE); + workspace.bottom_dock.update(cx, |bottom_dock, cx| { + bottom_dock.resize_active_panel(Some(size), cx); + }); +} + +fn resize_right_dock( + new_size: Pixels, + workspace: &mut Workspace, + cx: &mut ViewContext<'_, Workspace>, +) { + let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE); + workspace.right_dock.update(cx, |right_dock, cx| { + right_dock.resize_active_panel(Some(size), cx); + }); +} + +fn resize_left_dock( + new_size: Pixels, + workspace: &mut Workspace, + cx: &mut ViewContext<'_, Workspace>, +) { + let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE); + + workspace.left_dock.update(cx, |left_dock, cx| { + left_dock.resize_active_panel(Some(size), cx); + }); +} + impl WorkspaceStore { pub fn new(client: Arc, cx: &mut ModelContext) -> Self { Self { @@ -5926,7 +5972,7 @@ pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext let edge = cx.try_global::(); if new_edge != edge.map(|edge| edge.0) { cx.window_handle() - .update(cx, |workspace, cx| cx.notify(workspace.entity_id())) + .update(cx, |workspace, cx| cx.notify(Some(workspace.entity_id()))) .ok(); } }) diff --git a/crates/worktree/Cargo.toml b/crates/worktree/Cargo.toml index da3676f15c..adbbf66d23 100644 --- a/crates/worktree/Cargo.toml +++ b/crates/worktree/Cargo.toml @@ -37,7 +37,7 @@ log.workspace = true parking_lot.workspace = true paths.workspace = true postage.workspace = true -rpc.workspace = true +rpc = { workspace = true, features = ["gpui"] } schemars.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 28b23d2fa7..b7ee4466c7 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -7,7 +7,7 @@ use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{anyhow, Context as _, Result}; use clock::ReplicaId; use collections::{HashMap, HashSet, VecDeque}; -use fs::{copy_recursive, Fs, PathEvent, RemoveOptions, Watcher}; +use fs::{copy_recursive, Fs, MTime, PathEvent, RemoveOptions, Watcher}; use futures::{ channel::{ mpsc::{self, UnboundedSender}, @@ -29,6 +29,7 @@ use gpui::{ Task, }; use ignore::IgnoreStack; +use language::DiskState; use parking_lot::Mutex; use paths::local_settings_folder_relative_path; use postage::{ @@ -60,11 +61,14 @@ use std::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, }, - time::{Duration, Instant, SystemTime}, + time::{Duration, Instant}, }; use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; use text::{LineEnding, Rope}; -use util::{paths::home_dir, ResultExt}; +use util::{ + paths::{home_dir, PathMatcher}, + ResultExt, +}; pub use worktree_settings::WorktreeSettings; #[cfg(feature = "test-support")] @@ -133,6 +137,7 @@ pub struct RemoteWorktree { background_snapshot: Arc)>>, project_id: u64, client: AnyProtoClient, + file_scan_inclusions: PathMatcher, updates_tx: Option>, update_observer: Option>, snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>, @@ -149,6 +154,7 @@ pub struct Snapshot { root_char_bag: CharBag, entries_by_path: SumTree, entries_by_id: SumTree, + always_included_entries: Vec>, repository_entries: TreeMap, /// A number that increases every time the worktree begins scanning @@ -432,7 +438,7 @@ impl Worktree { cx.observe_global::(move |this, cx| { if let Self::Local(this) = this { let settings = WorktreeSettings::get(settings_location, cx).clone(); - if settings != this.settings { + if this.settings != settings { this.settings = settings; this.restart_background_scanners(cx); } @@ -479,11 +485,19 @@ impl Worktree { let (background_updates_tx, mut background_updates_rx) = mpsc::unbounded(); let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel(); + let worktree_id = snapshot.id(); + let settings_location = Some(SettingsLocation { + worktree_id, + path: Path::new(EMPTY_PATH), + }); + + let settings = WorktreeSettings::get(settings_location, cx).clone(); let worktree = RemoteWorktree { client, project_id, replica_id, snapshot, + file_scan_inclusions: settings.file_scan_inclusions.clone(), background_snapshot: background_snapshot.clone(), updates_tx: Some(background_updates_tx), update_observer: None, @@ -499,7 +513,10 @@ impl Worktree { while let Some(update) = background_updates_rx.next().await { { let mut lock = background_snapshot.lock(); - if let Err(error) = lock.0.apply_remote_update(update.clone()) { + if let Err(error) = lock + .0 + .apply_remote_update(update.clone(), &settings.file_scan_inclusions) + { log::error!("error applying worktree update: {}", error); } lock.1.push(update); @@ -1021,7 +1038,17 @@ impl LocalWorktree { let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded(); self.scan_requests_tx = scan_requests_tx; self.path_prefixes_to_scan_tx = path_prefixes_to_scan_tx; + self.start_background_scanner(scan_requests_rx, path_prefixes_to_scan_rx, cx); + let always_included_entries = mem::take(&mut self.snapshot.always_included_entries); + log::debug!( + "refreshing entries for the following always included paths: {:?}", + always_included_entries + ); + + // Cleans up old always included entries to ensure they get updated properly. Otherwise, + // nested always included entries may not get updated and will result in out-of-date info. + self.refresh_entries_for_paths(always_included_entries); } fn start_background_scanner( @@ -1313,9 +1340,10 @@ impl LocalWorktree { entry_id: None, worktree, path, - mtime: Some(metadata.mtime), + disk_state: DiskState::Present { + mtime: metadata.mtime, + }, is_local: true, - is_deleted: false, is_private, }) } @@ -1374,9 +1402,10 @@ impl LocalWorktree { entry_id: None, worktree, path, - mtime: Some(metadata.mtime), + disk_state: DiskState::Present { + mtime: metadata.mtime, + }, is_local: true, - is_deleted: false, is_private, }) } @@ -1512,10 +1541,11 @@ impl LocalWorktree { Ok(Arc::new(File { worktree, path, - mtime: Some(metadata.mtime), + disk_state: DiskState::Present { + mtime: metadata.mtime, + }, entry_id: None, is_local: true, - is_deleted: false, is_private, })) } @@ -1967,7 +1997,7 @@ impl RemoteWorktree { this.update(&mut cx, |worktree, _| { let worktree = worktree.as_remote_mut().unwrap(); let snapshot = &mut worktree.background_snapshot.lock().0; - let entry = snapshot.insert_entry(entry); + let entry = snapshot.insert_entry(entry, &worktree.file_scan_inclusions); worktree.snapshot = snapshot.clone(); entry })? @@ -2048,6 +2078,7 @@ impl Snapshot { abs_path, root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(), root_name, + always_included_entries: Default::default(), entries_by_path: Default::default(), entries_by_id: Default::default(), repository_entries: Default::default(), @@ -2111,8 +2142,12 @@ impl Snapshot { self.entries_by_id.get(&entry_id, &()).is_some() } - fn insert_entry(&mut self, entry: proto::Entry) -> Result { - let entry = Entry::try_from((&self.root_char_bag, entry))?; + fn insert_entry( + &mut self, + entry: proto::Entry, + always_included_paths: &PathMatcher, + ) -> Result { + let entry = Entry::try_from((&self.root_char_bag, always_included_paths, entry))?; let old_entry = self.entries_by_id.insert_or_replace( PathEntry { id: entry.id, @@ -2166,7 +2201,11 @@ impl Snapshot { } } - pub(crate) fn apply_remote_update(&mut self, mut update: proto::UpdateWorktree) -> Result<()> { + pub(crate) fn apply_remote_update( + &mut self, + mut update: proto::UpdateWorktree, + always_included_paths: &PathMatcher, + ) -> Result<()> { log::trace!( "applying remote worktree update. {} entries updated, {} removed", update.updated_entries.len(), @@ -2189,7 +2228,7 @@ impl Snapshot { } for entry in update.updated_entries { - let entry = Entry::try_from((&self.root_char_bag, entry))?; + let entry = Entry::try_from((&self.root_char_bag, always_included_paths, entry))?; if let Some(PathEntry { path, .. }) = self.entries_by_id.get(&entry.id, &()) { entries_by_path_edits.push(Edit::Remove(PathKey(path.clone()))); } @@ -2709,7 +2748,7 @@ impl LocalSnapshot { for entry in self.entries_by_path.cursor::<()>(&()) { if entry.is_file() { assert_eq!(files.next().unwrap().inode, entry.inode); - if !entry.is_ignored && !entry.is_external { + if (!entry.is_ignored && !entry.is_external) || entry.is_always_included { assert_eq!(visible_files.next().unwrap().inode, entry.inode); } } @@ -2792,7 +2831,7 @@ impl LocalSnapshot { impl BackgroundScannerState { fn should_scan_directory(&self, entry: &Entry) -> bool { - (!entry.is_external && !entry.is_ignored) + (!entry.is_external && (!entry.is_ignored || entry.is_always_included)) || entry.path.file_name() == Some(*DOT_GIT) || entry.path.file_name() == Some(local_settings_folder_relative_path().as_os_str()) || self.scanned_dirs.contains(&entry.id) // If we've ever scanned it, keep scanning @@ -3178,10 +3217,9 @@ impl fmt::Debug for Snapshot { pub struct File { pub worktree: Model, pub path: Arc, - pub mtime: Option, + pub disk_state: DiskState, pub entry_id: Option, pub is_local: bool, - pub is_deleted: bool, pub is_private: bool, } @@ -3194,8 +3232,8 @@ impl language::File for File { } } - fn mtime(&self) -> Option { - self.mtime + fn disk_state(&self) -> DiskState { + self.disk_state } fn path(&self) -> &Arc { @@ -3238,10 +3276,6 @@ impl language::File for File { self.worktree.read(cx).id() } - fn is_deleted(&self) -> bool { - self.is_deleted - } - fn as_any(&self) -> &dyn Any { self } @@ -3251,8 +3285,8 @@ impl language::File for File { worktree_id: self.worktree.read(cx).id().to_proto(), entry_id: self.entry_id.map(|id| id.to_proto()), path: self.path.to_string_lossy().into(), - mtime: self.mtime.map(|time| time.into()), - is_deleted: self.is_deleted, + mtime: self.disk_state.mtime().map(|time| time.into()), + is_deleted: self.disk_state == DiskState::Deleted, } } @@ -3293,10 +3327,13 @@ impl File { Arc::new(Self { worktree, path: entry.path.clone(), - mtime: entry.mtime, + disk_state: if let Some(mtime) = entry.mtime { + DiskState::Present { mtime } + } else { + DiskState::New + }, entry_id: Some(entry.id), is_local: true, - is_deleted: false, is_private: entry.is_private, }) } @@ -3316,13 +3353,22 @@ impl File { return Err(anyhow!("worktree id does not match file")); } + let disk_state = if proto.is_deleted { + DiskState::Deleted + } else { + if let Some(mtime) = proto.mtime.map(&Into::into) { + DiskState::Present { mtime } + } else { + DiskState::New + } + }; + Ok(Self { worktree, path: Path::new(&proto.path).into(), - mtime: proto.mtime.map(|time| time.into()), + disk_state, entry_id: proto.entry_id.map(ProjectEntryId::from_proto), is_local: false, - is_deleted: proto.is_deleted, is_private: false, }) } @@ -3336,21 +3382,20 @@ impl File { } pub fn project_entry_id(&self, _: &AppContext) -> Option { - if self.is_deleted { - None - } else { - self.entry_id + match self.disk_state { + DiskState::Deleted => None, + _ => self.entry_id, } } } -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Entry { pub id: ProjectEntryId, pub kind: EntryKind, pub path: Arc, pub inode: u64, - pub mtime: Option, + pub mtime: Option, pub canonical_path: Option>, /// Whether this entry is ignored by Git. @@ -3359,6 +3404,12 @@ pub struct Entry { /// exclude them from searches. pub is_ignored: bool, + /// Whether this entry is always included in searches. + /// + /// This is used for entries that are always included in searches, even + /// if they are ignored by git. Overridden by file_scan_exclusions. + pub is_always_included: bool, + /// Whether this entry's canonical path is outside of the worktree. /// This means the entry is only accessible from the worktree root via a /// symlink. @@ -3376,7 +3427,7 @@ pub struct Entry { pub is_fifo: bool, } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum EntryKind { UnloadedDir, PendingDir, @@ -3430,6 +3481,7 @@ impl Entry { size: metadata.len, canonical_path, is_ignored: false, + is_always_included: false, is_external: false, is_private: false, git_status: None, @@ -3476,7 +3528,8 @@ impl sum_tree::Item for Entry { type Summary = EntrySummary; fn summary(&self, _cx: &()) -> Self::Summary { - let non_ignored_count = if self.is_ignored || self.is_external { + let non_ignored_count = if (self.is_ignored || self.is_external) && !self.is_always_included + { 0 } else { 1 @@ -4244,6 +4297,7 @@ impl BackgroundScanner { if child_entry.is_dir() { child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, true); + child_entry.is_always_included = self.settings.is_path_always_included(&child_path); // Avoid recursing until crash in the case of a recursive symlink if job.ancestor_inodes.contains(&child_entry.inode) { @@ -4268,6 +4322,7 @@ impl BackgroundScanner { } } else { child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false); + child_entry.is_always_included = self.settings.is_path_always_included(&child_path); if !child_entry.is_ignored { if let Some(repo) = &containing_repository { if let Ok(repo_path) = child_entry.path.strip_prefix(&repo.work_directory) { @@ -4304,6 +4359,12 @@ impl BackgroundScanner { new_jobs.remove(job_ix); } } + if entry.is_always_included { + state + .snapshot + .always_included_entries + .push(entry.path.clone()); + } } state.populate_dir(&job.path, new_entries, new_ignore); @@ -4420,6 +4481,7 @@ impl BackgroundScanner { fs_entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, is_dir); fs_entry.is_external = is_external; fs_entry.is_private = self.is_path_private(path); + fs_entry.is_always_included = self.settings.is_path_always_included(path); if let (Some(scan_queue_tx), true) = (&scan_queue_tx, is_dir) { if state.should_scan_directory(&fs_entry) @@ -5307,7 +5369,7 @@ impl<'a> Traversal<'a> { if let Some(entry) = self.cursor.item() { if (self.include_files || !entry.is_file()) && (self.include_dirs || !entry.is_dir()) - && (self.include_ignored || !entry.is_ignored) + && (self.include_ignored || !entry.is_ignored || entry.is_always_included) { return true; } @@ -5438,10 +5500,12 @@ impl<'a> From<&'a Entry> for proto::Entry { } } -impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { +impl<'a> TryFrom<(&'a CharBag, &PathMatcher, proto::Entry)> for Entry { type Error = anyhow::Error; - fn try_from((root_char_bag, entry): (&'a CharBag, proto::Entry)) -> Result { + fn try_from( + (root_char_bag, always_included, entry): (&'a CharBag, &PathMatcher, proto::Entry), + ) -> Result { let kind = if entry.is_dir { EntryKind::Dir } else { @@ -5452,7 +5516,7 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { Ok(Entry { id: ProjectEntryId::from_proto(entry.id), kind, - path, + path: path.clone(), inode: entry.inode, mtime: entry.mtime.map(|time| time.into()), size: entry.size.unwrap_or(0), @@ -5460,6 +5524,7 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { .canonical_path .map(|path_string| Box::from(Path::new(&path_string))), is_ignored: entry.is_ignored, + is_always_included: always_included.is_match(path.as_ref()), is_external: entry.is_external, git_status: git_status_from_proto(entry.git_status), is_private: false, diff --git a/crates/worktree/src/worktree_settings.rs b/crates/worktree/src/worktree_settings.rs index 32851d963a..f26dc4af0f 100644 --- a/crates/worktree/src/worktree_settings.rs +++ b/crates/worktree/src/worktree_settings.rs @@ -9,6 +9,7 @@ use util::paths::PathMatcher; #[derive(Clone, PartialEq, Eq)] pub struct WorktreeSettings { + pub file_scan_inclusions: PathMatcher, pub file_scan_exclusions: PathMatcher, pub private_files: PathMatcher, } @@ -21,13 +22,19 @@ impl WorktreeSettings { pub fn is_path_excluded(&self, path: &Path) -> bool { path.ancestors() - .any(|ancestor| self.file_scan_exclusions.is_match(ancestor)) + .any(|ancestor| self.file_scan_exclusions.is_match(&ancestor)) + } + + pub fn is_path_always_included(&self, path: &Path) -> bool { + path.ancestors() + .any(|ancestor| self.file_scan_inclusions.is_match(&ancestor)) } } #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct WorktreeSettingsContent { - /// Completely ignore files matching globs from `file_scan_exclusions` + /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides + /// `file_scan_inclusions`. /// /// Default: [ /// "**/.git", @@ -42,6 +49,15 @@ pub struct WorktreeSettingsContent { #[serde(default)] pub file_scan_exclusions: Option>, + /// Always include files that match these globs when scanning for files, even if they're + /// ignored by git. This setting is overridden by `file_scan_exclusions`. + /// Default: [ + /// ".env*", + /// "docker-compose.*.yml", + /// ] + #[serde(default)] + pub file_scan_inclusions: Option>, + /// Treat the files matching these globs as `.env` files. /// Default: [ "**/.env*" ] pub private_files: Option>, @@ -59,11 +75,27 @@ impl Settings for WorktreeSettings { let result: WorktreeSettingsContent = sources.json_merge()?; let mut file_scan_exclusions = result.file_scan_exclusions.unwrap_or_default(); let mut private_files = result.private_files.unwrap_or_default(); + let mut parsed_file_scan_inclusions: Vec = result + .file_scan_inclusions + .unwrap_or_default() + .iter() + .flat_map(|glob| { + Path::new(glob) + .ancestors() + .map(|a| a.to_string_lossy().into()) + }) + .filter(|p| p != "") + .collect(); file_scan_exclusions.sort(); private_files.sort(); + parsed_file_scan_inclusions.sort(); Ok(Self { file_scan_exclusions: path_matchers(&file_scan_exclusions, "file_scan_exclusions")?, private_files: path_matchers(&private_files, "private_files")?, + file_scan_inclusions: path_matchers( + &parsed_file_scan_inclusions, + "file_scan_inclusions", + )?, }) } } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 75f86fa606..fbedd896e3 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -878,6 +878,211 @@ async fn test_write_file(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_file_scan_inclusions(cx: &mut TestAppContext) { + init_test(cx); + cx.executor().allow_parking(); + let dir = temp_tree(json!({ + ".gitignore": "**/target\n/node_modules\ntop_level.txt\n", + "target": { + "index": "blah2" + }, + "node_modules": { + ".DS_Store": "", + "prettier": { + "package.json": "{}", + }, + }, + "src": { + ".DS_Store": "", + "foo": { + "foo.rs": "mod another;\n", + "another.rs": "// another", + }, + "bar": { + "bar.rs": "// bar", + }, + "lib.rs": "mod foo;\nmod bar;\n", + }, + "top_level.txt": "top level file", + ".DS_Store": "", + })); + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |project_settings| { + project_settings.file_scan_exclusions = Some(vec![]); + project_settings.file_scan_inclusions = Some(vec![ + "node_modules/**/package.json".to_string(), + "**/.DS_Store".to_string(), + ]); + }); + }); + }); + + let tree = Worktree::local( + dir.path(), + true, + Arc::new(RealFs::default()), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + tree.flush_fs_events(cx).await; + tree.read_with(cx, |tree, _| { + // Assert that file_scan_inclusions overrides file_scan_exclusions. + check_worktree_entries( + tree, + &[], + &["target", "node_modules"], + &["src/lib.rs", "src/bar/bar.rs", ".gitignore"], + &[ + "node_modules/prettier/package.json", + ".DS_Store", + "node_modules/.DS_Store", + "src/.DS_Store", + ], + ) + }); +} + +#[gpui::test] +async fn test_file_scan_exclusions_overrules_inclusions(cx: &mut TestAppContext) { + init_test(cx); + cx.executor().allow_parking(); + let dir = temp_tree(json!({ + ".gitignore": "**/target\n/node_modules\n", + "target": { + "index": "blah2" + }, + "node_modules": { + ".DS_Store": "", + "prettier": { + "package.json": "{}", + }, + }, + "src": { + ".DS_Store": "", + "foo": { + "foo.rs": "mod another;\n", + "another.rs": "// another", + }, + }, + ".DS_Store": "", + })); + + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |project_settings| { + project_settings.file_scan_exclusions = Some(vec!["**/.DS_Store".to_string()]); + project_settings.file_scan_inclusions = Some(vec!["**/.DS_Store".to_string()]); + }); + }); + }); + + let tree = Worktree::local( + dir.path(), + true, + Arc::new(RealFs::default()), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + tree.flush_fs_events(cx).await; + tree.read_with(cx, |tree, _| { + // Assert that file_scan_inclusions overrides file_scan_exclusions. + check_worktree_entries( + tree, + &[".DS_Store, src/.DS_Store"], + &["target", "node_modules"], + &["src/foo/another.rs", "src/foo/foo.rs", ".gitignore"], + &[], + ) + }); +} + +#[gpui::test] +async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppContext) { + init_test(cx); + cx.executor().allow_parking(); + let dir = temp_tree(json!({ + ".gitignore": "**/target\n/node_modules/\n", + "target": { + "index": "blah2" + }, + "node_modules": { + ".DS_Store": "", + "prettier": { + "package.json": "{}", + }, + }, + "src": { + ".DS_Store": "", + "foo": { + "foo.rs": "mod another;\n", + "another.rs": "// another", + }, + }, + ".DS_Store": "", + })); + + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |project_settings| { + project_settings.file_scan_exclusions = Some(vec![]); + project_settings.file_scan_inclusions = Some(vec!["node_modules/**".to_string()]); + }); + }); + }); + let tree = Worktree::local( + dir.path(), + true, + Arc::new(RealFs::default()), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + tree.flush_fs_events(cx).await; + + tree.read_with(cx, |tree, _| { + assert!(tree + .entry_for_path("node_modules") + .is_some_and(|f| f.is_always_included)); + assert!(tree + .entry_for_path("node_modules/prettier/package.json") + .is_some_and(|f| f.is_always_included)); + }); + + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |project_settings| { + project_settings.file_scan_exclusions = Some(vec![]); + project_settings.file_scan_inclusions = Some(vec![]); + }); + }); + }); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + tree.flush_fs_events(cx).await; + + tree.read_with(cx, |tree, _| { + assert!(tree + .entry_for_path("node_modules") + .is_some_and(|f| !f.is_always_included)); + assert!(tree + .entry_for_path("node_modules/prettier/package.json") + .is_some_and(|f| !f.is_always_included)); + }); +} + #[gpui::test] async fn test_file_scan_exclusions(cx: &mut TestAppContext) { init_test(cx); @@ -939,6 +1144,7 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) { ], &["target", "node_modules"], &["src/lib.rs", "src/bar/bar.rs", ".gitignore"], + &[], ) }); @@ -970,6 +1176,7 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) { "src/.DS_Store", ".DS_Store", ], + &[], ) }); } @@ -1051,6 +1258,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { "src/bar/bar.rs", ".gitignore", ], + &[], ) }); @@ -1111,6 +1319,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { "src/new_file", ".gitignore", ], + &[], ) }); } @@ -1140,14 +1349,14 @@ async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) { .await; tree.flush_fs_events(cx).await; tree.read_with(cx, |tree, _| { - check_worktree_entries(tree, &[], &["HEAD", "foo"], &[]) + check_worktree_entries(tree, &[], &["HEAD", "foo"], &[], &[]) }); std::fs::write(dot_git_worktree_dir.join("new_file"), "new file contents") .unwrap_or_else(|e| panic!("Failed to create in {dot_git_worktree_dir:?} a new file: {e}")); tree.flush_fs_events(cx).await; tree.read_with(cx, |tree, _| { - check_worktree_entries(tree, &[], &["HEAD", "foo", "new_file"], &[]) + check_worktree_entries(tree, &[], &["HEAD", "foo", "new_file"], &[], &[]) }); } @@ -1180,8 +1389,12 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { let snapshot = Arc::new(Mutex::new(tree.snapshot())); tree.observe_updates(0, cx, { let snapshot = snapshot.clone(); + let settings = tree.settings().clone(); move |update| { - snapshot.lock().apply_remote_update(update).unwrap(); + snapshot + .lock() + .apply_remote_update(update, &settings.file_scan_inclusions) + .unwrap(); async { true } } }); @@ -1474,12 +1687,14 @@ async fn test_random_worktree_operations_during_initial_scan( snapshot }); + let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings()); + for (i, snapshot) in snapshots.into_iter().enumerate().rev() { let mut updated_snapshot = snapshot.clone(); for update in updates.lock().iter() { if update.scan_id >= updated_snapshot.scan_id() as u64 { updated_snapshot - .apply_remote_update(update.clone()) + .apply_remote_update(update.clone(), &settings.file_scan_inclusions) .unwrap(); } } @@ -1610,10 +1825,14 @@ async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) ); } + let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings()); + for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() { for update in updates.lock().iter() { if update.scan_id >= prev_snapshot.scan_id() as u64 { - prev_snapshot.apply_remote_update(update.clone()).unwrap(); + prev_snapshot + .apply_remote_update(update.clone(), &settings.file_scan_inclusions) + .unwrap(); } } @@ -2588,6 +2807,7 @@ fn check_worktree_entries( expected_excluded_paths: &[&str], expected_ignored_paths: &[&str], expected_tracked_paths: &[&str], + expected_included_paths: &[&str], ) { for path in expected_excluded_paths { let entry = tree.entry_for_path(path); @@ -2610,10 +2830,19 @@ fn check_worktree_entries( .entry_for_path(path) .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'")); assert!( - !entry.is_ignored, + !entry.is_ignored || entry.is_always_included, "expected path '{path}' to be tracked, but got entry: {entry:?}", ); } + for path in expected_included_paths { + let entry = tree + .entry_for_path(path) + .unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'")); + assert!( + entry.is_always_included, + "expected path '{path}' to always be included, but got entry: {entry:?}", + ); + } } fn init_test(cx: &mut gpui::TestAppContext) { diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 94fc4a338d..9dd106c063 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -2,7 +2,7 @@ description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.163.0" +version = "0.164.0" publish = false license = "GPL-3.0-or-later" authors = ["Zed Team "] @@ -15,14 +15,15 @@ name = "zed" path = "src/main.rs" [dependencies] -assistant_slash_command.workspace = true activity_indicator.workspace = true anyhow.workspace = true assets.workspace = true assistant.workspace = true +assistant2.workspace = true async-watch.workspace = true audio.workspace = true auto_update.workspace = true +auto_update_ui.workspace = true backtrace = "0.3" breadcrumbs.workspace = true call.workspace = true @@ -35,7 +36,6 @@ collab_ui.workspace = true collections.workspace = true command_palette.workspace = true command_palette_hooks.workspace = true -context_servers.workspace = true copilot.workspace = true debugger_ui.workspace = true debugger_tools.workspace = true @@ -43,6 +43,7 @@ db.workspace = true diagnostics.workspace = true editor.workspace = true env_logger.workspace = true +extension.workspace = true extension_host.workspace = true extensions_ui.workspace = true feature_flags.workspace = true @@ -57,12 +58,13 @@ go_to_line.workspace = true gpui = { workspace = true, features = ["wayland", "x11", "font-kit"] } http_client.workspace = true image_viewer.workspace = true -indexed_docs.workspace = true inline_completion_button.workspace = true install_cli.workspace = true journal.workspace = true language.workspace = true +language_extension.workspace = true language_model.workspace = true +language_models.workspace = true language_selector.workspace = true language_tools.workspace = true languages = { workspace = true, features = ["load-grammars"] } @@ -78,16 +80,17 @@ outline.workspace = true outline_panel.workspace = true parking_lot.workspace = true paths.workspace = true +picker.workspace = true profiling.workspace = true project.workspace = true project_panel.workspace = true project_symbols.workspace = true proto.workspace = true -quick_action_bar.workspace = true recent_projects.workspace = true release_channel.workspace = true remote.workspace = true repl.workspace = true +reqwest_client.workspace = true rope.workspace = true search.workspace = true serde.workspace = true @@ -108,16 +111,18 @@ tasks_ui.workspace = true telemetry_events.workspace = true terminal_view.workspace = true theme.workspace = true +theme_extension.workspace = true theme_selector.workspace = true time.workspace = true toolchain_selector.workspace = true ui.workspace = true -reqwest_client.workspace = true url.workspace = true urlencoding = "2.1.2" util.workspace = true uuid.workspace = true +vcs_menu.workspace = true vim.workspace = true +vim_mode_setting.workspace = true welcome.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 2d7b326807..ebc2e2e0a8 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -1,5 +1,3 @@ -// Allow binary to be called Zed for a nice application menu when running executable directly -#![allow(non_snake_case)] // Disable command line from opening on release mode #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] @@ -7,16 +5,15 @@ mod reliability; mod zed; use anyhow::{anyhow, Context as _, Result}; -use assistant_slash_command::SlashCommandRegistry; use chrono::Offset; use clap::{command, Parser}; use cli::FORCE_CLI_MODE_ENV_VAR_NAME; use client::{parse_zed_link, Client, ProxySettings, UserStore}; use collab_ui::channel_view::ChannelView; -use context_servers::ContextServerFactoryRegistry; use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE}; use editor::Editor; use env_logger::Builder; +use extension::ExtensionHostProxy; use fs::{Fs, RealFs}; use futures::{future, StreamExt}; use git::GitHostingProviderRegistry; @@ -25,7 +22,6 @@ use gpui::{ VisualContext, }; use http_client::{read_proxy_from_env, Uri}; -use indexed_docs::IndexedDocsRegistry; use language::LanguageRegistry; use log::LevelFilter; use reqwest_client::ReqwestClient; @@ -42,7 +38,6 @@ use settings::{ }; use simplelog::ConfigBuilder; use smol::process::Command; -use snippet_provider::SnippetRegistry; use std::{ env, fs::OpenOptions, @@ -66,7 +61,7 @@ use zed::{ OpenRequest, }; -use crate::zed::inline_completion_registry; +use crate::zed::{assistant_hints, inline_completion_registry}; #[cfg(feature = "mimalloc")] #[global_allocator] @@ -286,6 +281,9 @@ fn main() { OpenListener::set_global(cx, open_listener.clone()); + extension::init(cx); + let extension_host_proxy = ExtensionHostProxy::global(cx); + let client = Client::production(cx); cx.set_http_client(client.http_client().clone()); let mut languages = LanguageRegistry::new(cx.background_executor().clone()); @@ -319,6 +317,7 @@ fn main() { let node_runtime = NodeRuntime::new(client.http_client(), rx); language::init(cx); + language_extension::init(extension_host_proxy.clone(), languages.clone()); languages::init(languages.clone(), node_runtime.clone(), cx); let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx)); let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx)); @@ -330,7 +329,6 @@ fn main() { debugger_ui::init(cx); debugger_tools::init(cx); client::init(&client, cx); - language::init(cx); let telemetry = client.telemetry(); telemetry.start( system_id.as_ref().map(|id| id.to_string()), @@ -369,6 +367,7 @@ fn main() { AppState::set_global(Arc::downgrade(&app_state), cx); auto_update::init(client.http_client(), cx); + auto_update_ui::init(cx); reliability::init( client.http_client(), system_id.as_ref().map(|id| id.to_string()), @@ -379,6 +378,11 @@ fn main() { SystemAppearance::init(cx); theme::init(theme::LoadThemes::All(Box::new(Assets)), cx); + theme_extension::init( + extension_host_proxy.clone(), + ThemeRegistry::global(cx), + cx.background_executor().clone(), + ); command_palette::init(cx); let copilot_language_server_id = app_state.languages.next_language_server_id(); copilot::init( @@ -389,7 +393,8 @@ fn main() { cx, ); supermaven::init(app_state.client.clone(), cx); - language_model::init( + language_model::init(cx); + language_models::init( app_state.user_store.clone(), app_state.client.clone(), app_state.fs.clone(), @@ -403,22 +408,15 @@ fn main() { stdout_is_a_pty(), cx, ); + assistant2::init(cx); + assistant_hints::init(cx); repl::init( app_state.fs.clone(), app_state.client.telemetry().clone(), cx, ); - let api = extensions_ui::ConcreteExtensionRegistrationHooks::new( - ThemeRegistry::global(cx), - SlashCommandRegistry::global(cx), - IndexedDocsRegistry::global(cx), - SnippetRegistry::global(cx), - app_state.languages.clone(), - ContextServerFactoryRegistry::global(cx), - cx, - ); extension_host::init( - api, + extension_host_proxy, app_state.fs.clone(), app_state.client.clone(), app_state.node_runtime.clone(), @@ -461,6 +459,7 @@ fn main() { call::init(app_state.client.clone(), app_state.user_store.clone(), cx); notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); collab_ui::init(&app_state, cx); + vcs_menu::init(cx); feedback::init(cx); markdown_preview::init(cx); welcome::init(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 5d1b9cc762..7692b49c56 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1,14 +1,18 @@ mod app_menus; +pub mod assistant_hints; pub mod inline_completion_registry; #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub(crate) mod linux_prompts; #[cfg(target_os = "macos")] pub(crate) mod mac_only_instance; mod open_listener; +mod quick_action_bar; #[cfg(target_os = "windows")] pub(crate) mod windows_only_instance; +use anyhow::Context as _; pub use app_menus::*; +use assets::Assets; use assistant::PromptBuilder; use breadcrumbs::Breadcrumbs; use client::{zed_urls, ZED_URL_SCHEME}; @@ -18,17 +22,18 @@ use debugger_ui::debugger_panel::DebugPanel; use editor::ProposedChangesEditorToolbar; use editor::{scroll::Autoscroll, Editor, MultiBuffer}; use feature_flags::FeatureFlagAppExt; +use futures::{channel::mpsc, select_biased, StreamExt}; use gpui::{ actions, point, px, AppContext, AsyncAppContext, Context, FocusableView, MenuItem, PathPromptOptions, PromptLevel, ReadGlobal, Task, TitlebarOptions, View, ViewContext, VisualContext, WindowKind, WindowOptions, }; pub use open_listener::*; - -use anyhow::Context as _; -use assets::Assets; -use futures::{channel::mpsc, select_biased, StreamExt}; use outline_panel::OutlinePanel; +use paths::{ + local_debug_file_relative_path, local_settings_file_relative_path, + local_tasks_file_relative_path, +}; use project::{DirectoryLister, Item}; use project_panel::ProjectPanel; use quick_action_bar::QuickActionBar; @@ -43,19 +48,14 @@ use settings::{ use std::any::TypeId; use std::path::PathBuf; use std::{borrow::Cow, ops::Deref, path::Path, sync::Arc}; -use theme::ActiveTheme; -use workspace::notifications::NotificationId; -use workspace::CloseIntent; - -use paths::{ - local_debug_file_relative_path, local_settings_file_relative_path, - local_tasks_file_relative_path, -}; use terminal_view::terminal_panel::{self, TerminalPanel}; +use theme::ActiveTheme; use util::{asset_str, ResultExt}; use uuid::Uuid; -use vim::VimModeSetting; +use vim_mode_setting::VimModeSetting; use welcome::{BaseKeymap, MultibufferHint}; +use workspace::notifications::NotificationId; +use workspace::CloseIntent; use workspace::{ create_and_open_local_file, notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings, @@ -228,7 +228,7 @@ pub fn initialize_workspace( status_bar.add_right_item(cursor_position, cx); }); - auto_update::notify_of_any_new_update(cx); + auto_update_ui::notify_of_any_new_update(cx); let handle = cx.view().downgrade(); cx.on_window_should_close(move |cx| { @@ -242,11 +242,11 @@ pub fn initialize_workspace( .unwrap_or(true) }); + let release_channel = ReleaseChannel::global(cx); + let assistant2_feature_flag = cx.wait_for_flag::(); + let prompt_builder = prompt_builder.clone(); cx.spawn(|workspace_handle, mut cx| async move { - let assistant_panel = - assistant::AssistantPanel::load(workspace_handle.clone(), prompt_builder, cx.clone()); - let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone()); let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); @@ -264,7 +264,6 @@ pub fn initialize_workspace( project_panel, outline_panel, terminal_panel, - assistant_panel, channels_panel, chat_panel, notification_panel, @@ -273,7 +272,6 @@ pub fn initialize_workspace( project_panel, outline_panel, terminal_panel, - assistant_panel, channels_panel, chat_panel, notification_panel, @@ -281,7 +279,6 @@ pub fn initialize_workspace( )?; workspace_handle.update(&mut cx, |workspace, cx| { - workspace.add_panel(assistant_panel, cx); workspace.add_panel(project_panel, cx); workspace.add_panel(outline_panel, cx); workspace.add_panel(terminal_panel, cx); @@ -289,7 +286,34 @@ pub fn initialize_workspace( workspace.add_panel(chat_panel, cx); workspace.add_panel(notification_panel, cx); workspace.add_panel(debug_panel, cx); - cx.focus_self(); + })?; + let is_assistant2_enabled = + if cfg!(test) || release_channel != ReleaseChannel::Dev { + false + } else { + assistant2_feature_flag.await + } + ; + + let (assistant_panel, assistant2_panel) = if is_assistant2_enabled { + let assistant2_panel = + assistant2::AssistantPanel::load(workspace_handle.clone(), cx.clone()).await?; + + (None, Some(assistant2_panel)) + } else { + let assistant_panel = + assistant::AssistantPanel::load(workspace_handle.clone(), prompt_builder, cx.clone()).await?; + + (Some(assistant_panel), None) + }; + workspace_handle.update(&mut cx, |workspace, cx| { + if let Some(assistant_panel) = assistant_panel { + workspace.add_panel(assistant_panel, cx); + } + + if let Some(assistant2_panel) = assistant2_panel { + workspace.add_panel(assistant2_panel, cx); + } }) }) .detach(); @@ -846,8 +870,13 @@ pub fn handle_keymap_file_changes( }) .detach(); - cx.on_keyboard_layout_change(move |_| { - keyboard_layout_tx.unbounded_send(()).ok(); + let mut current_mapping = settings::get_key_equivalents(cx.keyboard_layout()); + cx.on_keyboard_layout_change(move |cx| { + let next_mapping = settings::get_key_equivalents(cx.keyboard_layout()); + if next_mapping != current_mapping { + current_mapping = next_mapping; + keyboard_layout_tx.unbounded_send(()).ok(); + } }) .detach(); @@ -3534,7 +3563,8 @@ mod tests { app_state.client.http_client().clone(), cx, ); - language_model::init( + language_model::init(cx); + language_models::init( app_state.user_store.clone(), app_state.client.clone(), app_state.fs.clone(), diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index 5c01724ba7..8586df57f2 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -23,7 +23,10 @@ pub fn app_menus() -> Vec { zed_actions::OpenDefaultKeymap, ), MenuItem::action("Open Project Settings", super::OpenProjectSettings), - MenuItem::action("Select Theme...", theme_selector::Toggle::default()), + MenuItem::action( + "Select Theme...", + zed_actions::theme_selector::Toggle::default(), + ), ], }), MenuItem::separator(), @@ -32,7 +35,7 @@ pub fn app_menus() -> Vec { items: vec![], }), MenuItem::separator(), - MenuItem::action("Extensions", extensions_ui::Extensions), + MenuItem::action("Extensions", zed_actions::Extensions), MenuItem::action("Install CLI", install_cli::Install), MenuItem::separator(), MenuItem::action("Hide Zed", super::Hide), @@ -50,7 +53,7 @@ pub fn app_menus() -> Vec { MenuItem::action("Open…", workspace::Open), MenuItem::action( "Open Recent...", - recent_projects::OpenRecent { + zed_actions::OpenRecent { create_new_window: true, }, ), @@ -146,7 +149,7 @@ pub fn app_menus() -> Vec { MenuItem::action("Back", workspace::GoBack), MenuItem::action("Forward", workspace::GoForward), MenuItem::separator(), - MenuItem::action("Command Palette...", command_palette::Toggle), + MenuItem::action("Command Palette...", zed_actions::command_palette::Toggle), MenuItem::separator(), MenuItem::action("Go to File...", workspace::ToggleFileFinder::default()), // MenuItem::action("Go to Symbol in Project", project_symbols::Toggle), @@ -176,7 +179,7 @@ pub fn app_menus() -> Vec { MenuItem::action("View Telemetry", zed_actions::OpenTelemetryLog), MenuItem::action("View Dependency Licenses", zed_actions::OpenLicenses), MenuItem::action("Show Welcome", workspace::Welcome), - MenuItem::action("Give Feedback...", feedback::GiveFeedback), + MenuItem::action("Give Feedback...", zed_actions::feedback::GiveFeedback), MenuItem::separator(), MenuItem::action( "Documentation", diff --git a/crates/zed/src/zed/assistant_hints.rs b/crates/zed/src/zed/assistant_hints.rs new file mode 100644 index 0000000000..244b7fab26 --- /dev/null +++ b/crates/zed/src/zed/assistant_hints.rs @@ -0,0 +1,115 @@ +use assistant::assistant_settings::AssistantSettings; +use collections::HashMap; +use editor::{ActiveLineTrailerProvider, Editor, EditorMode}; +use gpui::{AnyWindowHandle, AppContext, ViewContext, WeakView, WindowContext}; +use settings::{Settings, SettingsStore}; +use std::{cell::RefCell, rc::Rc}; +use theme::ActiveTheme; +use ui::prelude::*; +use workspace::Workspace; + +pub fn init(cx: &mut AppContext) { + let editors: Rc, AnyWindowHandle>>> = Rc::default(); + + cx.observe_new_views({ + let editors = editors.clone(); + move |_: &mut Workspace, cx: &mut ViewContext| { + let workspace_handle = cx.view().clone(); + cx.subscribe(&workspace_handle, { + let editors = editors.clone(); + move |_, _, event, cx| match event { + workspace::Event::ItemAdded { item } => { + if let Some(editor) = item.act_as::(cx) { + if editor.read(cx).mode() != EditorMode::Full { + return; + } + + cx.on_release({ + let editor_handle = editor.downgrade(); + let editors = editors.clone(); + move |_, _, _| { + editors.borrow_mut().remove(&editor_handle); + } + }) + .detach(); + editors + .borrow_mut() + .insert(editor.downgrade(), cx.window_handle()); + + let show_hints = should_show_hints(cx); + editor.update(cx, |editor, cx| { + assign_active_line_trailer_provider(editor, show_hints, cx) + }) + } + } + _ => {} + } + }) + .detach(); + } + }) + .detach(); + + let mut show_hints = AssistantSettings::get_global(cx).show_hints; + cx.observe_global::(move |cx| { + let new_show_hints = should_show_hints(cx); + if new_show_hints != show_hints { + show_hints = new_show_hints; + for (editor, window) in editors.borrow().iter() { + _ = window.update(cx, |_window, cx| { + _ = editor.update(cx, |editor, cx| { + assign_active_line_trailer_provider(editor, show_hints, cx); + }) + }); + } + } + }) + .detach(); +} + +struct AssistantHintsProvider; + +impl ActiveLineTrailerProvider for AssistantHintsProvider { + fn render_active_line_trailer( + &mut self, + style: &editor::EditorStyle, + focus_handle: &gpui::FocusHandle, + cx: &mut WindowContext, + ) -> Option { + if !focus_handle.is_focused(cx) { + return None; + } + + let chat_keybinding = + cx.keystroke_text_for_action_in(&assistant::ToggleFocus, focus_handle); + let generate_keybinding = + cx.keystroke_text_for_action_in(&zed_actions::InlineAssist::default(), focus_handle); + + Some( + h_flex() + .id("inline-assistant-instructions") + .w_full() + .font_family(style.text.font().family) + .text_color(cx.theme().status().hint) + .line_height(style.text.line_height) + .child(format!( + "{chat_keybinding} to chat, {generate_keybinding} to generate" + )) + .into_any(), + ) + } +} + +fn assign_active_line_trailer_provider( + editor: &mut Editor, + show_hints: bool, + cx: &mut ViewContext, +) { + let provider = show_hints.then_some(AssistantHintsProvider); + editor.set_active_line_trailer_provider(provider, cx); +} + +fn should_show_hints(cx: &AppContext) -> bool { + let assistant_settings = AssistantSettings::get_global(cx); + assistant_settings.enabled && assistant_settings.show_hints +} diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs similarity index 99% rename from crates/quick_action_bar/src/quick_action_bar.rs rename to crates/zed/src/zed/quick_action_bar.rs index 7849620093..85090a1b97 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -1,3 +1,6 @@ +mod markdown_preview; +mod repl_menu; + use assistant::assistant_settings::AssistantSettings; use assistant::AssistantPanel; use editor::actions::{ @@ -6,7 +9,6 @@ use editor::actions::{ SelectNext, SelectSmallerSyntaxNode, ToggleGoToLine, ToggleOutline, }; use editor::{Editor, EditorSettings}; - use gpui::{ Action, AnchorCorner, ClickEvent, ElementId, EventEmitter, FocusHandle, FocusableView, InteractiveElement, ParentElement, Render, Styled, Subscription, View, ViewContext, WeakView, @@ -22,9 +24,6 @@ use workspace::{ }; use zed_actions::InlineAssist; -mod repl_menu; -mod toggle_markdown_preview; - pub struct QuickActionBar { _inlay_hints_enabled_subscription: Option, active_item: Option>, diff --git a/crates/quick_action_bar/src/toggle_markdown_preview.rs b/crates/zed/src/zed/quick_action_bar/markdown_preview.rs similarity index 98% rename from crates/quick_action_bar/src/toggle_markdown_preview.rs rename to crates/zed/src/zed/quick_action_bar/markdown_preview.rs index 527da3a568..5162cb0644 100644 --- a/crates/quick_action_bar/src/toggle_markdown_preview.rs +++ b/crates/zed/src/zed/quick_action_bar/markdown_preview.rs @@ -5,7 +5,7 @@ use markdown_preview::{ use ui::{prelude::*, text_for_keystroke, IconButtonShape, Tooltip}; use workspace::Workspace; -use crate::QuickActionBar; +use super::QuickActionBar; impl QuickActionBar { pub fn render_toggle_markdown_preview( diff --git a/crates/quick_action_bar/src/repl_menu.rs b/crates/zed/src/zed/quick_action_bar/repl_menu.rs similarity index 99% rename from crates/quick_action_bar/src/repl_menu.rs rename to crates/zed/src/zed/quick_action_bar/repl_menu.rs index d2649d4180..5f616da9d3 100644 --- a/crates/quick_action_bar/src/repl_menu.rs +++ b/crates/zed/src/zed/quick_action_bar/repl_menu.rs @@ -1,5 +1,6 @@ use std::time::Duration; +use gpui::ElementId; use gpui::{percentage, Animation, AnimationExt, AnyElement, Transformation, View}; use picker::Picker; use repl::{ @@ -11,11 +12,9 @@ use ui::{ prelude::*, ButtonLike, ContextMenu, IconWithIndicator, Indicator, IntoElement, PopoverMenu, PopoverMenuHandle, Tooltip, }; - -use gpui::ElementId; use util::ResultExt; -use crate::QuickActionBar; +use super::QuickActionBar; const ZED_REPL_DOCUMENTATION: &str = "https://zed.dev/docs/repl"; @@ -402,7 +401,7 @@ fn session_state(session: View, cx: &WindowContext) -> ReplMenuState { status: session.kernel.status(), ..fill_fields() }, - Kernel::RunningKernel(kernel) => match &kernel.execution_state { + Kernel::RunningKernel(kernel) => match &kernel.execution_state() { ExecutionState::Idle => ReplMenuState { tooltip: format!("Run code on {} ({})", kernel_name, kernel_language).into(), indicator: Some(Indicator::dot().color(Color::Success)), diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 7ea5c923c2..b4bb6d2152 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -32,6 +32,7 @@ actions!( Quit, OpenKeymap, About, + Extensions, OpenLicenses, OpenTelemetryLog, DecreaseBufferFontSize, @@ -43,9 +44,88 @@ actions!( ] ); +pub mod branches { + use gpui::actions; + + actions!(branches, [OpenRecent]); +} + +pub mod command_palette { + use gpui::actions; + + actions!(command_palette, [Toggle]); +} + +pub mod feedback { + use gpui::actions; + + actions!(feedback, [GiveFeedback]); +} + +pub mod theme_selector { + use gpui::impl_actions; + use serde::Deserialize; + + #[derive(PartialEq, Clone, Default, Debug, Deserialize)] + pub struct Toggle { + /// A list of theme names to filter the theme selector down to. + pub themes_filter: Option>, + } + + impl_actions!(theme_selector, [Toggle]); +} + #[derive(Clone, Default, Deserialize, PartialEq)] pub struct InlineAssist { pub prompt: Option, } impl_actions!(assistant, [InlineAssist]); + +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct OpenRecent { + #[serde(default)] + pub create_new_window: bool, +} +gpui::impl_actions!(projects, [OpenRecent]); +gpui::actions!(projects, [OpenRemote]); + +/// Spawn a task with name or open tasks modal +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct Spawn { + #[serde(default)] + /// Name of the task to spawn. + /// If it is not set, a modal with a list of available tasks is opened instead. + /// Defaults to None. + pub task_name: Option, +} + +impl Spawn { + pub fn modal() -> Self { + Self { task_name: None } + } +} + +/// Rerun last task +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct Rerun { + /// Controls whether the task context is reevaluated prior to execution of a task. + /// If it is not, environment variables such as ZED_COLUMN, ZED_FILE are gonna be the same as in the last execution of a task + /// If it is, these variables will be updated to reflect current state of editor at the time task::Rerun is executed. + /// default: false + #[serde(default)] + pub reevaluate_context: bool, + /// Overrides `allow_concurrent_runs` property of the task being reran. + /// Default: null + #[serde(default)] + pub allow_concurrent_runs: Option, + /// Overrides `use_new_terminal` property of the task being reran. + /// Default: null + #[serde(default)] + pub use_new_terminal: Option, + + /// If present, rerun the task with this ID, otherwise rerun the last task. + pub task_id: Option, +} + +impl_actions!(task, [Spawn, Rerun]); diff --git a/docs/src/assistant/configuration.md b/docs/src/assistant/configuration.md index 2145bd9504..1be96491f4 100644 --- a/docs/src/assistant/configuration.md +++ b/docs/src/assistant/configuration.md @@ -200,18 +200,28 @@ You must provide the model's Context Window in the `max_tokens` parameter, this { "assistant": { "enabled": true, + "show_hints": true, + "button": true, + "dock": "right" + "default_width": 480, "default_model": { "provider": "zed.dev", "model": "claude-3-5-sonnet" }, "version": "2", - "button": true, - "default_width": 480, - "dock": "right" } } ``` +| key | type | default | description | +| -------------- | ------- | ------- | ------------------------------------------------------------------------------------- | +| enabled | boolean | true | Setting this to `false` will completely disable the assistant | +| show_hints | boolean | true | Whether to to show hints in the editor explaining how to use assistant | +| button | boolean | true | Show the assistant icon in the status bar | +| dock | string | "right" | The default dock position for the assistant panel. Can be ["left", "right", "bottom"] | +| default_height | string | null | The pixel height of the assistant panel when docked to the bottom | +| default_width | string | null | The pixel width of the assistant panel when docked to the left or right | + #### Custom endpoints {#custom-endpoint} You can use a custom API endpoint for different providers, as long as it's compatible with the providers API structure. @@ -271,13 +281,3 @@ will generate two outputs for every assist. One with Claude 3.5 Sonnet, and one } } ``` - -#### Common Panel Settings - -| key | type | default | description | -| -------------- | ------- | ------- | ------------------------------------------------------------------------------------- | -| enabled | boolean | true | Setting this to `false` will completely disable the assistant | -| button | boolean | true | Show the assistant icon in the status bar | -| dock | string | "right" | The default dock position for the assistant panel. Can be ["left", "right", "bottom"] | -| default_height | string | null | The pixel height of the assistant panel when docked to the bottom | -| default_width | string | null | The pixel width of the assistant panel when docked to the left or right | diff --git a/docs/src/assistant/prompting.md b/docs/src/assistant/prompting.md index 9a919816a6..19ae5d107a 100644 --- a/docs/src/assistant/prompting.md +++ b/docs/src/assistant/prompting.md @@ -138,7 +138,6 @@ Zed has the following internal prompt templates: - `content_prompt.hbs`: Used for generating content in the editor. - `terminal_assistant_prompt.hbs`: Used for the terminal assistant feature. - `suggest_edits.hbs`: Used for generating the model instructions for the XML Suggest Edits should return. -- `step_resolution.hbs`: Used for generating the step resolution prompt. At this point it is unknown if we will expand templates further to be user-creatable. @@ -146,78 +145,17 @@ At this point it is unknown if we will expand templates further to be user-creat > **Note:** It is not recommended to override templates unless you know what you are doing. Editing templates will break your assistant if done incorrectly. -Zed allows you to override the default prompts used for various assistant features by placing custom Handlebars (.hbs) templates in your `~/.config/zed/prompts/templates` directory. +Zed allows you to override the default prompts used for various assistant features by placing custom Handlebars (.hbs) templates in your `~/.config/zed/prompt_overrides` directory. The following templates can be overridden: -1. `content_prompt.hbs`: Used for generating content in the editor. - Format: +1. [`content_prompt.hbs`](https://github.com/zed-industries/zed/tree/main/assets/prompts/content_prompt.hbs): Used for generating content in the editor. - ```handlebars - You are an AI programming assistant. Your task is to - {{#if is_insert}}insert{{else}}rewrite{{/if}} - {{content_type}}{{#if language_name}} in {{language_name}}{{/if}} - based on the following context and user request. Context: - {{#if is_truncated}} - [Content truncated...] - {{/if}} - {{document_content}} - {{#if is_truncated}} - [Content truncated...] - {{/if}} +2. [`terminal_assistant_prompt.hbs`](https://github.com/zed-industries/zed/tree/main/assets/prompts/terminal_assistant_prompt.hbs): Used for the terminal assistant feature. - User request: - {{user_prompt}} +3. [`suggest_edits.hbs`](https://github.com/zed-industries/zed/tree/main/assets/prompts/suggest_edits.hbs): Used for generating the model instructions for the XML Suggest Edits should return. - {{#if rewrite_section}} - Please rewrite the section enclosed in - - tags. - {{else}} - Please insert your response at the - - tag. - {{/if}} - - Provide only the - {{content_type}} - content in your response, without any additional explanation. - ``` - -2. `terminal_assistant_prompt.hbs`: Used for the terminal assistant feature. - Format: - - ```handlebars - You are an AI assistant for a terminal emulator. Provide helpful responses to - user queries about terminal commands, file systems, and general computer - usage. System information: - Operating System: - {{os}} - - Architecture: - {{arch}} - {{#if shell}} - - Shell: - {{shell}} - {{/if}} - {{#if working_directory}} - - Current Working Directory: - {{working_directory}} - {{/if}} - - Latest terminal output: - {{#each latest_output}} - {{this}} - {{/each}} - - User query: - {{user_prompt}} - - Provide a clear and concise response to the user's query, considering the - given system information and latest terminal output if relevant. - ``` - -3. `suggest_edits.hbs`: Used for generating the model instructions for the XML Suggest Edits should return. - -4. `step_resolution.hbs`: Used for generating the step resolution prompt. +4. [`project_slash_command.hbs`](https://github.com/zed-industries/zed/tree/main/assets/prompts/project_slash_command.hbs) > **Note:** Be sure you want to override these, as you'll miss out on iteration on our built-in features. This should be primarily used when developing Zed. diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 3e713359c4..4991ff1119 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1416,6 +1416,14 @@ Or to set a `socks5` proxy: `boolean` values +## File Finder + +### Modal Max Width + +- Description: Max-width of the file finder modal. It can take one of these values: `small`, `medium`, `large`, `xlarge`, and `full`. +- Setting: `max_modal_width` +- Default: `small` + ## Preferred Line Length - Description: The column at which to soft-wrap lines, for buffers where soft-wrap is enabled. @@ -1620,7 +1628,7 @@ List of `integer` column numbers "button": false, "shell": {}, "toolbar": { - "title": true + "breadcrumbs": true }, "working_directory": "current_project_directory" } @@ -1938,7 +1946,7 @@ Disable with: ## Terminal: Toolbar -- Description: Whether or not to show various elements in the terminal toolbar. It only affects terminals placed in the editor pane. +- Description: Whether or not to show various elements in the terminal toolbar. - Setting: `toolbar` - Default: @@ -1946,7 +1954,7 @@ Disable with: { "terminal": { "toolbar": { - "title": true + "breadcrumbs": true } } } @@ -1954,7 +1962,13 @@ Disable with: **Options** -At the moment, only the `title` option is available, it controls displaying of the terminal title that can be changed via `PROMPT_COMMAND`. If the title is hidden, the terminal toolbar is not displayed. +At the moment, only the `breadcrumbs` option is available, it controls displaying of the terminal title that can be changed via `PROMPT_COMMAND`. + +If the terminal title is empty, the breadcrumbs won't be shown. + +The shell running in the terminal needs to be configured to emit the title. + +Example command to set the title: `echo -e "\e]2;New Title\007";` ### Terminal: Button @@ -2319,15 +2333,18 @@ Run the `theme selector: toggle` action in the command palette to see a current - Default: ```json -"assistant": { - "enabled": true, - "button": true, - "dock": "right", - "default_width": 640, - "default_height": 320, - "provider": "openai", - "version": "1", -}, +{ + "assistant": { + "enabled": true, + "button": true, + "dock": "right", + "default_width": 640, + "default_height": 320, + "provider": "openai", + "version": "1", + "show_hints": true + } +} ``` ## Outline Panel diff --git a/docs/src/extensions/languages.md b/docs/src/extensions/languages.md index b7e0cb4482..0995ed97fd 100644 --- a/docs/src/extensions/languages.md +++ b/docs/src/extensions/languages.md @@ -20,26 +20,28 @@ path_suffixes = ["myl"] line_comments = ["# "] ``` -- `name` is the human readable name that will show up in the Select Language dropdown. -- `grammar` is the name of a grammar. Grammars are registered separately, described below. -- `path_suffixes` (optional) is an array of file suffixes that should be associated with this language. This supports glob patterns like `config/**/*.toml` where `**` matches 0 or more directories and `*` matches 0 or more characters. -- `line_comments` (optional) is an array of strings that are used to identify line comments in the language. +- `name` (required) is the human readable name that will show up in the Select Language dropdown. +- `grammar` (required) is the name of a grammar. Grammars are registered separately, described below. +- `path_suffixes` is an array of file suffixes that should be associated with this language. Unlike `file_types` in settings, this does not support glob patterns. +- `line_comments` is an array of strings that are used to identify line comments in the language. This is used for the `editor::ToggleComments` keybind: `{#kb editor::ToggleComments}` for toggling lines of code. +- `tab_size` defines the indentation/tab size used for this language (default is `4`). +- `hard_tabs` whether to indent with tabs (`true`) or spaces (`false`, the default). +- `first_line_pattern` is a regular expression, that in addition to `path_suffixes` (above) or `file_types` in settings can be used to match files which should use this language. For example Zed uses this to identify Shell Scripts by matching the [shebangs lines](https://github.com/zed-industries/zed/blob/main/crates/languages/src/bash/config.toml) in the first line of a script. ## Grammar diff --git a/docs/src/languages/dart.md b/docs/src/languages/dart.md index 32f312e5dd..6b7d01c39e 100644 --- a/docs/src/languages/dart.md +++ b/docs/src/languages/dart.md @@ -5,9 +5,22 @@ Dart support is available through the [Dart extension](https://github.com/zed-ex - Tree Sitter: [UserNobody14/tree-sitter-dart](https://github.com/UserNobody14/tree-sitter-dart) - Language Server: [dart language-server](https://github.com/dart-lang/sdk) +## Pre-requisites + +You will need to install the Dart SDK. + +You can install dart from [dart.dev/get-dart](https://dart.dev/get-dart) or via the [Flutter Version Management CLI (fvm)](https://fvm.app/documentation/getting-started/installation) + ## Configuration -The `dart` binary can be configured in a Zed settings file with: +The dart extension requires no configuration if you have `dart` in your path: + +```sh +which dart +dart --version +``` + +If you would like to use a specific dart binary or use dart via FVM you can specify the `dart` binary in your Zed settings.jsons file: ```json { @@ -22,7 +35,4 @@ The `dart` binary can be configured in a Zed settings file with: } ``` - +Please see the Dart documentation for more information on [dart language-server capabilities](https://github.com/dart-lang/sdk/blob/main/pkg/analysis_server/tool/lsp_spec/README.md). diff --git a/docs/src/languages/json.md b/docs/src/languages/json.md index 31fc8c0689..3783dcae2c 100644 --- a/docs/src/languages/json.md +++ b/docs/src/languages/json.md @@ -30,8 +30,47 @@ To workaround this behavior you can add the following to your `.prettierrc` } ``` +## JSON Language Server + +Zed automatically out of the box supports JSON Schema validation of `package.json` and `tsconfig.json` files, but `json-language-server` can use JSON Schema definitions in project files, from the [JSON Schema Store](https://www.schemastore.org/json/) or other publicly available URLs for JSON validation. + +### Inline Schema Specification + +To specify a schema inline with your JSON files, add a `$schema` top level key linking to your json schema file. + +For example to for a `.luarc.json` for use with [lua-language-server](https://github.com/LuaLS/lua-language-server/): + +```json +{ + "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", + "runtime.version": "Lua 5.4" +} +``` + +### Schema Specification via Settings + +You can alternatively associate JSON Schemas with file paths by via Zed LSP settings. + +To + +```json +"lsp": { +"json-language-server": { + "settings": { + "json": { + "schemas": [ + { + "fileMatch": ["*/*.luarc.json"], + "url": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json" + } + ] + } + } +} +``` + +You can also pass any of the [supported settings](https://github.com/Microsoft/vscode/blob/main/extensions/json-language-features/server/README.md#settings) to json-language-server by specifying them in your Zed settings.json: + diff --git a/docs/src/languages/ocaml.md b/docs/src/languages/ocaml.md index 10ffee86dd..65f7a68cda 100644 --- a/docs/src/languages/ocaml.md +++ b/docs/src/languages/ocaml.md @@ -1,6 +1,6 @@ # OCaml -OCaml support is available through the [OCaml extension](https://github.com/zed-industries/zed/tree/main/extensions/ocaml). +OCaml support is available through the [OCaml extension](https://github.com/zed-extensions/ocaml). - Tree Sitter: [tree-sitter/tree-sitter-ocaml](https://github.com/tree-sitter/tree-sitter-ocaml) - Language Server: [ocaml/ocaml-lsp](https://github.com/ocaml/ocaml-lsp) diff --git a/docs/src/languages/proto.md b/docs/src/languages/proto.md index 934080a1d7..777fd81b8a 100644 --- a/docs/src/languages/proto.md +++ b/docs/src/languages/proto.md @@ -1,9 +1,44 @@ # Proto -Proto/proto3 (Protocol Buffers definition language) support is available natively in Zed. +Proto/proto3 (Protocol Buffers definition language) support is available through the [Proto extension](https://github.com/zed-industries/zed/tree/main/extensions/proto). - Tree Sitter: [coder3101/tree-sitter-proto](https://github.com/coder3101/tree-sitter-proto) -- Language Server: [protols](https://github.com/coder3101/protols) +- Language Servers: [protobuf-language-server](https://github.com/lasorda/protobuf-language-server) + + diff --git a/docs/src/languages/svelte.md b/docs/src/languages/svelte.md index 157a57d43e..df7343d569 100644 --- a/docs/src/languages/svelte.md +++ b/docs/src/languages/svelte.md @@ -1,6 +1,6 @@ # Svelte -Svelte support is available through the [Svelte extension](https://github.com/zed-industries/zed/tree/main/extensions/svelte). +Svelte support is available through the [Svelte extension](https://github.com/zed-extensions/svelte). - Tree Sitter: [tree-sitter-grammars/tree-sitter-svelte](https://github.com/tree-sitter-grammars/tree-sitter-svelte) - Language Server: [sveltejs/language-tools](https://github.com/sveltejs/language-tools) diff --git a/docs/src/remote-development.md b/docs/src/remote-development.md index 17ae23bb63..7ab0cb6b76 100644 --- a/docs/src/remote-development.md +++ b/docs/src/remote-development.md @@ -125,7 +125,7 @@ Each connection tries to run the development server in proxy mode. This mode wil In the case that reconnecting fails, the daemon will not be re-used. That said, unsaved changes are by default persisted locally, so that you do not lose work. You can always reconnect to the project at a later date and Zed will restore unsaved changes. -If you are struggling with connection issues, you should be able to see more information in the Zed log `cmd-shift-p Open Log`. If you are seeing things that are unexpected, please file a [GitHub issue](https://github.com/zed-industries/zed/issues/new) or reach out in the #remoting-feedback channel in the [Zed Discord](https://discord.gg/zed-community). +If you are struggling with connection issues, you should be able to see more information in the Zed log `cmd-shift-p Open Log`. If you are seeing things that are unexpected, please file a [GitHub issue](https://github.com/zed-industries/zed/issues/new) or reach out in the #remoting-feedback channel in the [Zed Discord](https://zed.dev/community-links). ## Supported SSH Options @@ -152,4 +152,4 @@ Note that we deliberately disallow some options (for example `-t` or `-T`) that ## Feedback -Please join the #remoting-feedback channel in the [Zed Discord](https://discord.gg/zed-community). +Please join the #remoting-feedback channel in the [Zed Discord](https://zed.dev/community-links). diff --git a/docs/src/tasks.md b/docs/src/tasks.md index 98cbd6dfc1..f32e5778fc 100644 --- a/docs/src/tasks.md +++ b/docs/src/tasks.md @@ -41,7 +41,11 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to // "args": ["--login"] // } // } - "shell": "system" + "shell": "system", + // Whether to show the task line in the output of the spawned task, defaults to `true`. + "show_summary": true, + // Whether to show the command line in the output of the spawned task, defaults to `true`. + "show_output": true } ] ``` diff --git a/docs/src/windows.md b/docs/src/windows.md index 47fae7cb9f..f8949f22f3 100644 --- a/docs/src/windows.md +++ b/docs/src/windows.md @@ -10,4 +10,4 @@ Zed Employees are not currently working on the Windows build. However, we welcome contributions from the community to improve Windows support. - [GitHub Issues with 'Windows' label](https://github.com/zed-industries/zed/issues?q=is%3Aissue+is%3Aopen+label%3Awindows) -- [Zed Community Discord](https://discord.gg/zed-community) -> `#windows-port` +- [Zed Community Discord](https://zed.dev/community-links) -> `#windows-port` diff --git a/extensions/ocaml/Cargo.toml b/extensions/ocaml/Cargo.toml deleted file mode 100644 index 6df98bec4c..0000000000 --- a/extensions/ocaml/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "zed_ocaml" -version = "0.1.0" -edition = "2021" -publish = false -license = "Apache-2.0" - -[lints] -workspace = true - -[lib] -path = "src/ocaml.rs" -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = "0.1.0" diff --git a/extensions/ocaml/LICENSE-APACHE b/extensions/ocaml/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/extensions/ocaml/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/ocaml/extension.toml b/extensions/ocaml/extension.toml deleted file mode 100644 index bff7c380b5..0000000000 --- a/extensions/ocaml/extension.toml +++ /dev/null @@ -1,25 +0,0 @@ -id = "ocaml" -name = "OCaml" -description = "OCaml support." -version = "0.1.0" -schema_version = 1 -authors = ["Rashid Almheiri <69181766+huwaireb@users.noreply.github.com>"] -repository = "https://github.com/zed-industries/zed" - -[language_servers.ocamllsp] -name = "ocamllsp" -languages = ["OCaml", "OCaml Interface"] - -[grammars.ocaml] -repository = "https://github.com/tree-sitter/tree-sitter-ocaml" -commit = "0b12614ded3ec7ed7ab7933a9ba4f695ba4c342e" -path = "grammars/ocaml" - -[grammars.ocaml_interface] -repository = "https://github.com/tree-sitter/tree-sitter-ocaml" -commit = "0b12614ded3ec7ed7ab7933a9ba4f695ba4c342e" -path = "grammars/interface" - -[grammars.dune] -repository = "https://github.com/WHForks/tree-sitter-dune" -commit = "b3f7882e1b9a1d8811011bf6f0de1c74c9c93949" diff --git a/extensions/ocaml/languages/dune/config.toml b/extensions/ocaml/languages/dune/config.toml deleted file mode 100644 index b4f79850b6..0000000000 --- a/extensions/ocaml/languages/dune/config.toml +++ /dev/null @@ -1,8 +0,0 @@ -name = "Dune" -grammar = "dune" -path_suffixes = ["dune", "dune-project"] -brackets = [ - { start = "(", end = ")", close = true, newline = true }, - { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] } -] -tab_size = 2 diff --git a/extensions/ocaml/languages/dune/highlights.scm b/extensions/ocaml/languages/dune/highlights.scm deleted file mode 100644 index e7a21cd2c5..0000000000 --- a/extensions/ocaml/languages/dune/highlights.scm +++ /dev/null @@ -1,5 +0,0 @@ -(stanza_name) @function -(field_name) @property -(quoted_string) @string -(multiline_string) @string -(action_name) @keyword diff --git a/extensions/ocaml/languages/dune/injections.scm b/extensions/ocaml/languages/dune/injections.scm deleted file mode 100644 index 654b5b2c13..0000000000 --- a/extensions/ocaml/languages/dune/injections.scm +++ /dev/null @@ -1,2 +0,0 @@ -((ocaml_syntax) @injection.content - (#set! injection.language "ocaml")) diff --git a/extensions/ocaml/languages/ocaml-interface/brackets.scm b/extensions/ocaml/languages/ocaml-interface/brackets.scm deleted file mode 100644 index e7e8145eba..0000000000 --- a/extensions/ocaml/languages/ocaml-interface/brackets.scm +++ /dev/null @@ -1,2 +0,0 @@ -("(" @open ")" @close) -("{" @open "}" @close) diff --git a/extensions/ocaml/languages/ocaml-interface/config.toml b/extensions/ocaml/languages/ocaml-interface/config.toml deleted file mode 100644 index a4378ec3ed..0000000000 --- a/extensions/ocaml/languages/ocaml-interface/config.toml +++ /dev/null @@ -1,12 +0,0 @@ -name = "OCaml Interface" -code_fence_block_name = "ocaml" -grammar = "ocaml_interface" -path_suffixes = ["mli"] -block_comment = ["(* ", " *)"] -autoclose_before = ";,=)}" -brackets = [ - { start = "{", end = "}", close = true, newline = true }, - { start = "[", end = "]", close = true, newline = true }, - { start = "(", end = ")", close = true, newline = true } -] -tab_size = 2 diff --git a/extensions/ocaml/languages/ocaml-interface/highlights.scm b/extensions/ocaml/languages/ocaml-interface/highlights.scm deleted file mode 120000 index e6f0d00d1d..0000000000 --- a/extensions/ocaml/languages/ocaml-interface/highlights.scm +++ /dev/null @@ -1 +0,0 @@ -../ocaml/highlights.scm \ No newline at end of file diff --git a/extensions/ocaml/languages/ocaml-interface/indents.scm b/extensions/ocaml/languages/ocaml-interface/indents.scm deleted file mode 100644 index 0de50a48bb..0000000000 --- a/extensions/ocaml/languages/ocaml-interface/indents.scm +++ /dev/null @@ -1,21 +0,0 @@ -[ - (type_binding) - - (value_specification) - (method_specification) - - (external) - (field_declaration) -] @indent - -(_ "<" ">" @end) @indent -(_ "{" "}" @end) @indent -(_ "(" ")" @end) @indent - -(_ "object" @start "end" @end) @indent - -(signature - "sig" @start - "end" @end) @indent - -";;" @outdent diff --git a/extensions/ocaml/languages/ocaml-interface/outline.scm b/extensions/ocaml/languages/ocaml-interface/outline.scm deleted file mode 100644 index b8539d4cd0..0000000000 --- a/extensions/ocaml/languages/ocaml-interface/outline.scm +++ /dev/null @@ -1,48 +0,0 @@ -(module_type_definition - "module" @context - "type" @context - name: (_) @name) @item - -(module_definition - "module" @context - (module_binding name: (_) @name)) @item - -(type_definition - "type" @context - (type_binding name: (_) @name)) @item - -(class_definition - "class" @context - (class_binding - "virtual"? @context - name: (_) @name)) @item - -(class_type_definition - "class" @context - "type" @context - (class_type_binding - "virtual"? @context - name: (_) @name)) @item - -(instance_variable_definition - "val" @context - "method"? @context - name: (_) @name) @item - -(method_specification - "method" @context - "virtual"? @context - (method_name) @name) @item - -(value_specification - "val" @context - (value_name) @name) @item - -(external - "external" @context - (value_name) @name) @item - -(exception_definition - "exception" @context - (constructor_declaration - (constructor_name) @name)) @item diff --git a/extensions/ocaml/languages/ocaml/brackets.scm b/extensions/ocaml/languages/ocaml/brackets.scm deleted file mode 100644 index 1f5ee9bfa3..0000000000 --- a/extensions/ocaml/languages/ocaml/brackets.scm +++ /dev/null @@ -1,5 +0,0 @@ -("(" @open ")" @close) -("[" @open "]" @close) -("[|" @open "|]" @close) -("{" @open "}" @close) -("\"" @open "\"" @close) diff --git a/extensions/ocaml/languages/ocaml/config.toml b/extensions/ocaml/languages/ocaml/config.toml deleted file mode 100644 index 7d5b4348d6..0000000000 --- a/extensions/ocaml/languages/ocaml/config.toml +++ /dev/null @@ -1,14 +0,0 @@ -name = "OCaml" -grammar = "ocaml" -path_suffixes = ["ml"] -block_comment = ["(* ", " *)"] -autoclose_before = ";,=)}]" -brackets = [ - { start = "{", end = "}", close = true, newline = true }, - { start = "{|", end = "|", close = true, newline = true, not_in = ["string"] }, - { start = "[", end = "]", close = true, newline = true }, - { start = "[|", end = "|", close = true, newline = true, not_in = ["string"] }, - { start = "(", end = ")", close = true, newline = true }, - { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] } -] -tab_size = 2 diff --git a/extensions/ocaml/languages/ocaml/highlights.scm b/extensions/ocaml/languages/ocaml/highlights.scm deleted file mode 100644 index 8029d3cc22..0000000000 --- a/extensions/ocaml/languages/ocaml/highlights.scm +++ /dev/null @@ -1,158 +0,0 @@ -; Modules -;-------- - -[(module_name) (module_type_name)] @title - -; Types -;------ - -[(class_name) (class_type_name) (type_constructor)] @type - -(tag) @variant ;; Polymorphic Variants -(constructor_name) @constructor ;; Exceptions, variants and the like - -; Functions -;---------- - -(let_binding - pattern: (value_name) @function - (parameter)) - -(let_binding - pattern: (value_name) @function - body: [(fun_expression) (function_expression)]) - -(value_specification (value_name) @function) - -(external (value_name) @function) - -(method_name) @function - -(infix_expression - left: (value_path (value_name) @function) - operator: (concat_operator) @operator - (#eq? @operator "@@")) - -(infix_expression - operator: (rel_operator) @operator - right: (value_path (value_name) @function) - (#eq? @operator "|>")) - -(application_expression - function: (value_path (value_name) @function)) - -; Variables -;---------- - -(value_pattern) @variable - -(type_variable) @variable.special - -; Properties -;----------- - -[(field_name) (instance_variable_name)] @property - -; Labels -;------- - -[(label_name) (parameter)] @label - -(parameter - pattern: (value_pattern) @label) -; despite the above rule, we should still label value_pattern as a variable -; when a label name is present -(parameter - (label_name) - pattern: (value_pattern) @variable) - -; Constants -;---------- - -(boolean) @boolean - -[(number) (signed_number)] @number - -[(string) (character)] @string - -(quoted_string "{" @string "}" @string) @string -(quoted_string_content) @string - - -(escape_sequence) @string.escape - -[ - (conversion_specification) - (pretty_printing_indication) -] @punctuation.special - -; Operators -;---------- - -(match_expression (match_operator) @keyword) - -(value_definition [(let_operator) (let_and_operator)] @keyword) - -[ - (prefix_operator) - (sign_operator) - (pow_operator) - (mult_operator) - (add_operator) - (concat_operator) - (rel_operator) - (and_operator) - (or_operator) - (assign_operator) - (hash_operator) - (indexing_operator) - (let_operator) - (let_and_operator) - (match_operator) -] @operator - -["*" "#" "::" "<-"] @operator - -; Keywords -;--------- - -[ - "and" "as" "assert" "begin" "class" "constraint" "do" "done" "downto" "else" - "end" "exception" "external" "for" "fun" "function" "functor" "if" "in" - "include" "inherit" "initializer" "lazy" "let" "match" "method" "module" - "mutable" "new" "nonrec" "object" "of" "open" "private" "rec" "sig" "struct" - "then" "to" "try" "type" "val" "virtual" "when" "while" "with" -] @keyword - -; Punctuation -;------------ - -["(" ")" "[" "]" "{" "}" "[|" "|]" "[<" "[>" "[@@" "[@" "[%"] @punctuation.bracket - -(object_type ["<" ">"] @punctuation.bracket) - -[ - "," "." ";" ":" "=" "|" "~" "?" "+" "-" "!" ">" "&" - "->" ";;" ":>" "+=" ":=" ".." -] @punctuation.delimiter - -; Attributes -;----------- - -[ - (attribute) - (item_attribute) - (floating_attribute) - (extension) - (item_extension) - (quoted_extension) - (quoted_item_extension) - -] @attribute - -(attribute_id) @tag - -; Comments -;--------- - -[(comment) (line_number_directive) (directive) (shebang)] @comment diff --git a/extensions/ocaml/languages/ocaml/indents.scm b/extensions/ocaml/languages/ocaml/indents.scm deleted file mode 100644 index 319d2fd971..0000000000 --- a/extensions/ocaml/languages/ocaml/indents.scm +++ /dev/null @@ -1,45 +0,0 @@ -[ - (let_binding) - (type_binding) - - (method_definition) - - (external) - (value_specification) - (method_specification) - - (match_case) - - (function_expression) - - (field_declaration) - (field_expression) - - (application_expression) -] @indent - -(_ "[" "]" @end) @indent -(_ "[|" "|]" @end) @indent -(_ "<" ">" @end) @indent -(_ "{" "}" @end) @indent -(_ "(" ")" @end) @indent - -(_ "object" @start "end" @end) @indent - -(structure - "struct" @start - "end" @end) @indent - -(signature - "sig" @start - "end" @end) @indent - -(parenthesized_expression - "begin" @start - "end") @indent - -(do_clause - "do" @start - "done" @end) @indent - -";;" @outdent diff --git a/extensions/ocaml/languages/ocaml/outline.scm b/extensions/ocaml/languages/ocaml/outline.scm deleted file mode 100644 index c7f39c219b..0000000000 --- a/extensions/ocaml/languages/ocaml/outline.scm +++ /dev/null @@ -1,59 +0,0 @@ -(_structure_item/value_definition - "let" @context - (let_binding - pattern: (_) @name)) @item - -(_structure_item/exception_definition - "exception" @context - (constructor_declaration - (constructor_name) @name)) @item - -(_structure_item/module_definition - "module" @context - (module_binding - name: (module_name) @name)) @item - -(module_type_definition - "module" @context - "type" @context - name: (_) @name) @item - -(type_definition - "type" @context - (type_binding name: (_) @name)) @item - -(value_specification - "val" @context - (value_name) @name) @item - -(class_definition - "class" @context - (class_binding - "virtual"? @context - name: (_) @name)) @item - -(class_type_definition - "class" @context - "type" @context - (class_type_binding - "virtual"? @context - name: (_) @name)) @item - -(instance_variable_definition - "val" @context - "method"? @context - name: (_) @name) @item - -(method_specification - "method" @context - "virtual"? @context - (method_name) @name) @item - -(method_definition - "method" @context - "virtual"? @context - name: (_) @name) @item - -(external - "external" @context - (value_name) @name) @item diff --git a/extensions/ocaml/src/ocaml.rs b/extensions/ocaml/src/ocaml.rs deleted file mode 100644 index 94e6d55e17..0000000000 --- a/extensions/ocaml/src/ocaml.rs +++ /dev/null @@ -1,219 +0,0 @@ -use std::ops::Range; -use zed::lsp::{Completion, CompletionKind, Symbol, SymbolKind}; -use zed::{CodeLabel, CodeLabelSpan}; -use zed_extension_api::{self as zed, Result}; - -const OPERATOR_CHAR: [char; 17] = [ - '~', '!', '?', '%', '<', ':', '.', '$', '&', '*', '+', '-', '/', '=', '>', '@', '^', -]; - -struct OcamlExtension; - -impl zed::Extension for OcamlExtension { - fn new() -> Self { - Self - } - - fn language_server_command( - &mut self, - _language_server_id: &zed::LanguageServerId, - worktree: &zed::Worktree, - ) -> Result { - let path = worktree.which("ocamllsp").ok_or_else(|| { - "ocamllsp (ocaml-language-server) must be installed manually.".to_string() - })?; - - Ok(zed::Command { - command: path, - args: Vec::new(), - env: worktree.shell_env(), - }) - } - - fn label_for_completion( - &self, - _language_server_id: &zed::LanguageServerId, - completion: Completion, - ) -> Option { - let name = &completion.label; - let detail = completion.detail.as_ref().map(|s| s.replace('\n', " ")); - - match completion.kind.zip(detail) { - Some((CompletionKind::Constructor | CompletionKind::EnumMember, detail)) => { - let (argument, return_t) = detail - .split_once("->") - .map_or((None, detail.as_str()), |(arg, typ)| { - (Some(arg.trim()), typ.trim()) - }); - - let type_decl = "type t = "; - let type_of = argument.map(|_| " of ").unwrap_or_default(); - let argument = argument.unwrap_or_default(); - let terminator = "\n"; - let let_decl = "let _ "; - let let_colon = ": "; - let let_suffix = " = ()"; - let code = format!( - "{type_decl}{name}{type_of}{argument}{terminator}{let_decl}{let_colon}{return_t}{let_suffix}" - ); - - let name_start = type_decl.len(); - let argument_end = name_start + name.len() + type_of.len() + argument.len(); - let colon_start = argument_end + terminator.len() + let_decl.len(); - let return_type_end = code.len() - let_suffix.len(); - Some(CodeLabel { - code, - spans: vec![ - CodeLabelSpan::code_range(name_start..argument_end), - CodeLabelSpan::code_range(colon_start..return_type_end), - ], - filter_range: (0..name.len()).into(), - }) - } - - Some((CompletionKind::Field, detail)) => { - let filter_range_start = if name.starts_with(['~', '?']) { 1 } else { 0 }; - - let record_prefix = "type t = { "; - let record_suffix = "; }"; - let code = format!("{record_prefix}{name} : {detail}{record_suffix}"); - - Some(CodeLabel { - spans: vec![CodeLabelSpan::code_range( - record_prefix.len()..code.len() - record_suffix.len(), - )], - code, - filter_range: (filter_range_start..name.len()).into(), - }) - } - - Some((CompletionKind::Value, detail)) => { - let let_prefix = "let "; - let suffix = " = ()"; - let (l_paren, r_paren) = if name.contains(OPERATOR_CHAR) { - ("( ", " )") - } else { - ("", "") - }; - let code = format!("{let_prefix}{l_paren}{name}{r_paren} : {detail}{suffix}"); - - let name_start = let_prefix.len() + l_paren.len(); - let name_end = name_start + name.len(); - let type_annotation_start = name_end + r_paren.len(); - let type_annotation_end = code.len() - suffix.len(); - - Some(CodeLabel { - spans: vec![ - CodeLabelSpan::code_range(name_start..name_end), - CodeLabelSpan::code_range(type_annotation_start..type_annotation_end), - ], - filter_range: (0..name.len()).into(), - code, - }) - } - - Some((CompletionKind::Method, detail)) => { - let method_decl = "class c : object method "; - let end = " end"; - let code = format!("{method_decl}{name} : {detail}{end}"); - - Some(CodeLabel { - spans: vec![CodeLabelSpan::code_range( - method_decl.len()..code.len() - end.len(), - )], - code, - filter_range: (0..name.len()).into(), - }) - } - - Some((kind, _)) => { - let highlight_name = match kind { - CompletionKind::Module | CompletionKind::Interface => "title", - CompletionKind::Keyword => "keyword", - CompletionKind::TypeParameter => "type", - _ => return None, - }; - - Some(CodeLabel { - spans: vec![(CodeLabelSpan::literal(name, Some(highlight_name.to_string())))], - filter_range: (0..name.len()).into(), - code: String::new(), - }) - } - _ => None, - } - } - - fn label_for_symbol( - &self, - _language_server_id: &zed::LanguageServerId, - symbol: Symbol, - ) -> Option { - let name = &symbol.name; - - let (code, filter_range, display_range) = match symbol.kind { - SymbolKind::Property => { - let code = format!("type t = {{ {}: (); }}", name); - let filter_range: Range = 0..name.len(); - let display_range = 11..11 + name.len(); - (code, filter_range, display_range) - } - SymbolKind::Function - if name.contains(OPERATOR_CHAR) - || (name.starts_with("let") && name.contains(OPERATOR_CHAR)) => - { - let code = format!("let ( {name} ) () = ()"); - - let filter_range = 6..6 + name.len(); - let display_range = 0..filter_range.end + 1; - (code, filter_range, display_range) - } - SymbolKind::Function => { - let code = format!("let {name} () = ()"); - - let filter_range = 4..4 + name.len(); - let display_range = 0..filter_range.end; - (code, filter_range, display_range) - } - SymbolKind::Constructor => { - let code = format!("type t = {name}"); - let filter_range = 0..name.len(); - let display_range = 9..9 + name.len(); - (code, filter_range, display_range) - } - SymbolKind::Module => { - let code = format!("module {name} = struct end"); - let filter_range = 7..7 + name.len(); - let display_range = 0..filter_range.end; - (code, filter_range, display_range) - } - SymbolKind::Class => { - let code = format!("class {name} = object end"); - let filter_range = 6..6 + name.len(); - let display_range = 0..filter_range.end; - (code, filter_range, display_range) - } - SymbolKind::Method => { - let code = format!("class c = object method {name} = () end"); - let filter_range = 0..name.len(); - let display_range = 17..24 + name.len(); - (code, filter_range, display_range) - } - SymbolKind::String => { - let code = format!("type {name} = T"); - let filter_range = 5..5 + name.len(); - let display_range = 0..filter_range.end; - (code, filter_range, display_range) - } - _ => return None, - }; - - Some(CodeLabel { - code, - spans: vec![CodeLabelSpan::code_range(display_range)], - filter_range: filter_range.into(), - }) - } -} - -zed::register_extension!(OcamlExtension); diff --git a/extensions/toml/languages/toml/config.toml b/extensions/toml/languages/toml/config.toml index d5c1172d84..f62290d9e9 100644 --- a/extensions/toml/languages/toml/config.toml +++ b/extensions/toml/languages/toml/config.toml @@ -1,6 +1,6 @@ name = "TOML" grammar = "toml" -path_suffixes = ["Cargo.lock", "toml", "Pipfile"] +path_suffixes = ["Cargo.lock", "toml", "Pipfile", "uv.lock"] line_comments = ["# "] autoclose_before = ",]}" brackets = [ diff --git a/script/analyze_highlights.py b/script/analyze_highlights.py index 1fd16f2c0f..09a6419653 100644 --- a/script/analyze_highlights.py +++ b/script/analyze_highlights.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 """ This script analyzes all the highlight.scm files in our embedded languages and extensions. It counts the number of unique instances of @{name} and the languages in which they are used. diff --git a/script/bundle-linux b/script/bundle-linux index 2aa1dcab4a..98b49ae4da 100755 --- a/script/bundle-linux +++ b/script/bundle-linux @@ -69,7 +69,9 @@ strip --strip-debug "${target_dir}/${remote_server_triple}/release/remote_server # Ensure that remote_server does not depend on libssl nor libcrypto, as we got rid of these deps. -! ldd "${target_dir}/${remote_server_triple}/release/remote_server" | grep -q 'libcrypto\|libssl' +if ldd "${target_dir}/${remote_server_triple}/release/remote_server" | grep -q 'libcrypto\|libssl'; then + echo "Error: remote_server still depends on libssl or libcrypto" && exit 1 +fi suffix="" if [ "$channel" != "stable" ]; then @@ -89,8 +91,8 @@ cp "${target_dir}/${target_triple}/release/cli" "${zed_dir}/bin/zed" # Libs find_libs() { ldd ${target_dir}/${target_triple}/release/zed |\ - cut -d' ' -f3 |\ - grep -v '\<\(libstdc++.so\|libc.so\|libgcc_s.so\|libm.so\|libpthread.so\|libdl.so\)' + cut -d' ' -f3 |\ + grep -v '\<\(libstdc++.so\|libc.so\|libgcc_s.so\|libm.so\|libpthread.so\|libdl.so\)' } mkdir -p "${zed_dir}/lib" diff --git a/script/bundle-mac b/script/bundle-mac index 06231a22ab..54247645cc 100755 --- a/script/bundle-mac +++ b/script/bundle-mac @@ -172,11 +172,6 @@ function download_git() { x86_64-apple-darwin) download_and_unpack "https://github.com/desktop/dugite-native/releases/download/${GIT_VERSION}/dugite-native-${GIT_VERSION}-${GIT_VERSION_SHA}-macOS-x64.tar.gz" bin/git ./git ;; - universal) - download_and_unpack "https://github.com/desktop/dugite-native/releases/download/${GIT_VERSION}/dugite-native-${GIT_VERSION}-${GIT_VERSION_SHA}-macOS-arm64.tar.gz" bin/git ./git_arm64 - download_and_unpack "https://github.com/desktop/dugite-native/releases/download/${GIT_VERSION}/dugite-native-${GIT_VERSION}-${GIT_VERSION_SHA}-macOS-x64.tar.gz" bin/git ./git_x64 - lipo -create ./git_arm64 ./git_x64 -output ./git - ;; *) echo "Unsupported architecture: $architecture" exit 1 @@ -377,20 +372,7 @@ else prepare_binaries "aarch64-apple-darwin" "$app_path_aarch64" prepare_binaries "x86_64-apple-darwin" "$app_path_x64" - cp -R "$app_path_x64" target/release/ - app_path=target/release/$(basename "$app_path_x64") - lipo \ - -create \ - target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/zed \ - -output \ - "${app_path}/Contents/MacOS/zed" - lipo \ - -create \ - target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/cli \ - -output \ - "${app_path}/Contents/MacOS/cli" - sign_app_binaries "$app_path" "universal" "." sign_app_binaries "$app_path_x64" "x86_64-apple-darwin" "x86_64-apple-darwin" sign_app_binaries "$app_path_aarch64" "aarch64-apple-darwin" "aarch64-apple-darwin" diff --git a/script/clear-target-dir-if-larger-than b/script/clear-target-dir-if-larger-than index d23c111ec1..691ff42ffd 100755 --- a/script/clear-target-dir-if-larger-than +++ b/script/clear-target-dir-if-larger-than @@ -2,7 +2,7 @@ set -eu -if [[ $# < 1 ]]; then +if [[ $# -ne 1 ]]; then echo "usage: $0 " exit 1 fi diff --git a/script/create-draft-release b/script/create-draft-release new file mode 100755 index 0000000000..95b1a1450a --- /dev/null +++ b/script/create-draft-release @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +preview="" +if [[ "$GITHUB_REF_NAME" == *"-pre" ]]; then + preview="-p" +fi + +gh release create -t "$GITHUB_REF_NAME" -d "$GITHUB_REF_NAME" -F "$1" $preview diff --git a/script/deploy-postgrest b/script/deploy-postgrest index 14fbd50e30..2a0b21a991 100755 --- a/script/deploy-postgrest +++ b/script/deploy-postgrest @@ -3,7 +3,7 @@ set -eu source script/lib/deploy-helpers.sh -if [[ $# < 1 ]]; then +if [[ $# != 1 ]]; then echo "Usage: $0 (postgrest not needed on preview or nightly)" exit 1 fi diff --git a/script/draft-release-notes b/script/draft-release-notes index 287997ff79..1ef276718d 100755 --- a/script/draft-release-notes +++ b/script/draft-release-notes @@ -19,24 +19,45 @@ async function main() { process.exit(1); } - let priorVersion = [parts[0], parts[1], parts[2] - 1].join("."); - let suffix = ""; - - if (channel == "preview") { - suffix = "-pre"; - if (parts[2] == 0) { - priorVersion = [parts[0], parts[1] - 1, 0].join("."); - } - } else if (!ensureTag(`v${priorVersion}`)) { - console.log("Copy the release notes from preview."); + // currently we can only draft notes for patch releases. + if (parts[2] == 0) { process.exit(0); } + let priorVersion = [parts[0], parts[1], parts[2] - 1].join("."); + let suffix = channel == "preview" ? "-pre" : ""; let [tag, priorTag] = [`v${version}${suffix}`, `v${priorVersion}${suffix}`]; - if (!ensureTag(tag) || !ensureTag(priorTag)) { - console.log("Could not draft release notes, missing a tag:", tag, priorTag); - process.exit(0); + try { + execFileSync("rm", ["-rf", "target/shallow_clone"]); + execFileSync("git", [ + "clone", + "https://github.com/zed-industries/zed", + "target/shallow_clone", + "--filter=tree:0", + "--no-checkout", + "--branch", + tag, + "--depth", + 100, + ]); + execFileSync("git", [ + "-C", + "target/shallow_clone", + "rev-parse", + "--verify", + tag, + ]); + execFileSync("git", [ + "-C", + "target/shallow_clone", + "rev-parse", + "--verify", + priorTag, + ]); + } catch (e) { + console.error(e.stderr.toString()); + process.exit(1); } const newCommits = getCommits(priorTag, tag); @@ -64,16 +85,18 @@ async function main() { } console.log(releaseNotes.join("\n") + "\n"); - console.log(""); } function getCommits(oldTag, newTag) { const pullRequestNumbers = execFileSync( "git", - ["log", `${oldTag}..${newTag}`, "--format=DIVIDER\n%H|||%B"], + [ + "-C", + "target/shallow_clone", + "log", + `${oldTag}..${newTag}`, + "--format=DIVIDER\n%H|||%B", + ], { encoding: "utf8" }, ) .replace(/\r\n/g, "\n") @@ -103,18 +126,3 @@ function getCommits(oldTag, newTag) { return pullRequestNumbers; } - -function ensureTag(tag) { - try { - execFileSync("git", ["rev-parse", "--verify", tag]); - return true; - } catch (e) { - try { - execFileSync("git"[("fetch", "origin", "--shallow-exclude", tag)]); - execFileSync("git"[("fetch", "origin", "--deepen", "1")]); - return true; - } catch (e) { - return false; - } - } -} diff --git a/script/get-crate-version b/script/get-crate-version index b6346b32ec..0a35e4d49d 100755 --- a/script/get-crate-version +++ b/script/get-crate-version @@ -2,7 +2,7 @@ set -eu -if [[ $# < 1 ]]; then +if [[ $# -ne 1 ]]; then echo "Usage: $0 " >&2 exit 1 fi @@ -14,4 +14,4 @@ cargo metadata \ --format-version=1 \ | jq \ --raw-output \ - ".packages[] | select(.name == \"${CRATE_NAME}\") | .version" \ No newline at end of file + ".packages[] | select(.name == \"${CRATE_NAME}\") | .version" diff --git a/script/install-cmake b/script/install-cmake index 71b5aaeeef..3a28aae1b8 100755 --- a/script/install-cmake +++ b/script/install-cmake @@ -35,7 +35,7 @@ CMAKE_VERSION="${CMAKE_VERSION:-${1:-3.30.4}}" if [ "$(whoami)" = root ]; then SUDO=; else SUDO="$(command -v sudo || command -v doas || true)"; fi -if cmake --version | grep -q "$CMAKE_VERSION"; then +if cmake --version 2>/dev/null | grep -q "$CMAKE_VERSION"; then echo "CMake $CMAKE_VERSION is already installed." exit 0 elif [ -e /usr/local/bin/cmake ]; then @@ -51,7 +51,7 @@ elif [ -e /etc/lsb-release ] && grep -qP 'DISTRIB_ID=Ubuntu' /etc/lsb-release; t echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $(lsb_release -cs) main" \ | $SUDO tee /etc/apt/sources.list.d/kitware.list >/dev/null $SUDO apt-get update - $SUDO apt-get install -y kitware-archive-keyring cmake==$CMAKE_VERSION + $SUDO apt-get install -y kitware-archive-keyring cmake else arch="$(uname -m)" if [ "$arch" != "x86_64" ] && [ "$arch" != "aarch64" ]; then diff --git a/script/kube-shell b/script/kube-shell index 9181dc959c..0ca77acdd0 100755 --- a/script/kube-shell +++ b/script/kube-shell @@ -1,6 +1,6 @@ #!/bin/bash -if [[ $# < 1 ]]; then +if [[ $# -ne 1 ]]; then echo "Usage: $0 [production|staging|...]" exit 1 fi @@ -8,4 +8,4 @@ fi export ZED_KUBE_NAMESPACE=$1 pod=$(kubectl --namespace=${ZED_KUBE_NAMESPACE} get pods --selector=app=zed --output=jsonpath='{.items[*].metadata.name}') -exec kubectl --namespace $ZED_KUBE_NAMESPACE exec --tty --stdin $pod -- /bin/bash \ No newline at end of file +exec kubectl --namespace $ZED_KUBE_NAMESPACE exec --tty --stdin $pod -- /bin/bash diff --git a/script/metal-debug b/script/metal-debug index 6fc18e5ebd..de8476f3e3 100755 --- a/script/metal-debug +++ b/script/metal-debug @@ -10,4 +10,4 @@ export GPUProfilerEnabled="YES" export METAL_DEBUG_ERROR_MODE=0 export LD_LIBRARY_PATH="/Applications/Xcode.app/Contents/Developer/../SharedFrameworks/" -cargo run $@ +cargo run "$@" diff --git a/script/shellcheck-scripts b/script/shellcheck-scripts new file mode 100755 index 0000000000..d42b31d02f --- /dev/null +++ b/script/shellcheck-scripts @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -euo pipefail + +mode=${1:-error} +[[ "$mode" =~ ^(error|warning)$ ]] || { echo "Usage: $0 [error|warning]"; exit 1; } + +cd "$(dirname "$0")/.." || exit 1 + +find script -maxdepth 1 -type f -print0 | + xargs -0 grep -l -E '^#!(/bin/|/usr/bin/env )(sh|bash|dash)' | + xargs -r shellcheck -x -S "$mode" -C diff --git a/script/update-json-schemas b/script/update-json-schemas new file mode 100755 index 0000000000..182e0ff03b --- /dev/null +++ b/script/update-json-schemas @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd "$(dirname "$0")/.." || exit 1 +cd crates/languages/src/json/schemas +files=( + "tsconfig.json" + "package.json" +) +for file in "${files[@]}"; do + curl -sL -o "$file" "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/$file" +done + +HASH="$(curl -s 'https://api.github.com/repos/SchemaStore/schemastore/commits/HEAD' | jq -r '.sha')" +SHORT_HASH="${HASH:0:7}" +DATE="$(curl -s 'https://api.github.com/repos/SchemaStore/schemastore/commits/HEAD' |jq -r .commit.author.date | cut -c1-10)" +echo +echo "Updated JSON schemas to [SchemaStore/schemastore@$SHORT_HASH](https://github.com/SchemaStore/schemastore/tree/$HASH) ($DATE)" +echo +for file in "${files[@]}"; do + echo "- [$file](https://github.com/SchemaStore/schemastore/commits/master/src/schemas/json/$file)" \ + "@ [$SHORT_HASH](https://raw.githubusercontent.com/SchemaStore/schemastore/$HASH/src/schemas/json/$file)" +done +echo diff --git a/script/upload-nightly b/script/upload-nightly index 61b73d4e56..87ad712ae4 100755 --- a/script/upload-nightly +++ b/script/upload-nightly @@ -19,12 +19,12 @@ if [[ -n "${1:-}" ]]; then target="$1" else echo "Error: Target '$1' is not allowed" - echo "Usage: $0 [${allowed_targets[@]}]" + echo "Usage: $0 [${allowed_targets[*]}]" exit 1 fi else echo "Error: Target is not specified" -echo "Usage: $0 [${allowed_targets[@]}]" +echo "Usage: $0 [${allowed_targets[*]}]" exit 1 fi echo "Uploading nightly for target: $target" @@ -43,7 +43,6 @@ case "$target" in macos) upload_to_blob_store $bucket_name "target/aarch64-apple-darwin/release/Zed.dmg" "nightly/Zed-aarch64.dmg" upload_to_blob_store $bucket_name "target/x86_64-apple-darwin/release/Zed.dmg" "nightly/Zed-x86_64.dmg" - upload_to_blob_store $bucket_name "target/release/Zed.dmg" "nightly/Zed.dmg" upload_to_blob_store $bucket_name "target/latest-sha" "nightly/latest-sha" rm -f "target/aarch64-apple-darwin/release/Zed.dmg" "target/x86_64-apple-darwin/release/Zed.dmg" "target/release/Zed.dmg" rm -f "target/latest-sha"