diff --git a/.github/actions/run_tests/action.yml b/.github/actions/run_tests/action.yml index 07284e2f58..df714be003 100644 --- a/.github/actions/run_tests/action.yml +++ b/.github/actions/run_tests/action.yml @@ -10,7 +10,7 @@ runs: cargo install cargo-nextest - name: Install Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 with: node-version: "18" diff --git a/.github/workflows/bump_patch_version.yml b/.github/workflows/bump_patch_version.yml index e3468274a1..8bbefe64e8 100644 --- a/.github/workflows/bump_patch_version.yml +++ b/.github/workflows/bump_patch_version.yml @@ -43,6 +43,8 @@ jobs: esac which cargo-set-version > /dev/null || cargo install cargo-edit output=$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //') + export GIT_COMMITTER_NAME="Zed Bot" + export GIT_COMMITTER_EMAIL="hi@zed.dev" git commit -am "Bump to $output for @$GITHUB_ACTOR" --author "Zed Bot " git tag v${output}${tag_suffix} git push origin HEAD v${output}${tag_suffix} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84ed0dd5d4..e78b24255c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -232,7 +232,7 @@ jobs: DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} steps: - name: Install Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 with: node-version: "18" diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 132af3bda2..897d4b47c5 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -21,7 +21,7 @@ jobs: version: 9 - name: Setup Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 with: node-version: "20" cache: "pnpm" diff --git a/.github/workflows/deploy_cloudflare.yml b/.github/workflows/deploy_cloudflare.yml index e948eb64c3..d91eeb0f57 100644 --- a/.github/workflows/deploy_cloudflare.yml +++ b/.github/workflows/deploy_cloudflare.yml @@ -37,28 +37,28 @@ jobs: mdbook build ./docs --dest-dir=../target/deploy/docs/ - name: Deploy Docs - uses: cloudflare/wrangler-action@9681c2997648301493e78cacbfb790a9f19c833f # v3 + uses: cloudflare/wrangler-action@b2a0191ce60d21388e1a8dcc968b4e9966f938e1 # v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: pages deploy target/deploy --project-name=docs - name: Deploy Install - uses: cloudflare/wrangler-action@9681c2997648301493e78cacbfb790a9f19c833f # v3 + uses: cloudflare/wrangler-action@b2a0191ce60d21388e1a8dcc968b4e9966f938e1 # v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: r2 object put -f script/install.sh zed-open-source-website-assets/install.sh - name: Deploy Docs Workers - uses: cloudflare/wrangler-action@9681c2997648301493e78cacbfb790a9f19c833f # v3 + uses: cloudflare/wrangler-action@b2a0191ce60d21388e1a8dcc968b4e9966f938e1 # v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: deploy .cloudflare/docs-proxy/src/worker.js - name: Deploy Install Workers - uses: cloudflare/wrangler-action@9681c2997648301493e78cacbfb790a9f19c833f # v3 + uses: cloudflare/wrangler-action@b2a0191ce60d21388e1a8dcc968b4e9966f938e1 # v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} diff --git a/.github/workflows/randomized_tests.yml b/.github/workflows/randomized_tests.yml index dd7163dc5e..1ecf511100 100644 --- a/.github/workflows/randomized_tests.yml +++ b/.github/workflows/randomized_tests.yml @@ -22,7 +22,7 @@ jobs: - buildjet-16vcpu-ubuntu-2204 steps: - name: Install Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 with: node-version: "18" diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 534855cd21..8e409f4947 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -70,7 +70,7 @@ jobs: ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }} steps: - name: Install Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 with: node-version: "18" diff --git a/Cargo.lock b/Cargo.lock index b4a1682bc6..7b72a9d736 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,7 +9,7 @@ dependencies = [ "anyhow", "auto_update", "editor", - "extension", + "extension_host", "futures 0.3.30", "gpui", "language", @@ -544,9 +544,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.13" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e614738943d3f68c628ae3dbce7c3daffb196665f82f8c8ea6b65de73c79429" +checksum = "0cb8f1d480b0ea3783ab015936d2a55c87e219676f0c0b7dec61494043f21857" dependencies = [ "deflate64", "flate2", @@ -4182,6 +4182,51 @@ dependencies = [ [[package]] name = "extension" version = "0.1.0" +dependencies = [ + "anyhow", + "async-compression", + "async-tar", + "collections", + "fs", + "futures 0.3.30", + "http_client", + "language", + "log", + "lsp", + "semantic_version", + "serde", + "serde_json", + "toml 0.8.19", + "wasm-encoder 0.215.0", + "wasmparser 0.215.0", + "wit-component", +] + +[[package]] +name = "extension_cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "env_logger 0.11.5", + "extension", + "fs", + "language", + "log", + "reqwest_client", + "rpc", + "serde", + "serde_json", + "theme", + "tokio", + "toml 0.8.19", + "tree-sitter", + "wasmtime", +] + +[[package]] +name = "extension_host" +version = "0.1.0" dependencies = [ "anyhow", "assistant_slash_command", @@ -4192,6 +4237,7 @@ dependencies = [ "collections", "ctor", "env_logger 0.11.5", + "extension", "fs", "futures 0.3.30", "gpui", @@ -4219,36 +4265,11 @@ dependencies = [ "ui", "url", "util", - "wasm-encoder 0.215.0", - "wasmparser 0.215.0", "wasmtime", "wasmtime-wasi", - "wit-component", "workspace", ] -[[package]] -name = "extension_cli" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "env_logger 0.11.5", - "extension", - "fs", - "language", - "log", - "reqwest_client", - "rpc", - "serde", - "serde_json", - "theme", - "tokio", - "toml 0.8.19", - "tree-sitter", - "wasmtime", -] - [[package]] name = "extensions_ui" version = "0.1.0" @@ -4258,7 +4279,7 @@ dependencies = [ "collections", "db", "editor", - "extension", + "extension_host", "fs", "fuzzy", "gpui", @@ -4478,9 +4499,9 @@ checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" [[package]] name = "flume" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", @@ -5675,7 +5696,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2 0.5.7", "tokio", "tower-service", "tracing", @@ -6247,9 +6268,9 @@ dependencies = [ [[package]] name = "jupyter-serde" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a444fb3f87ee6885eb316028cc998c7d84811663ef95d78c419419423d5a054" +checksum = "77b96de099fc23d5c21e05de32cc087c8326983895b7f6c242562af01f7d4c81" dependencies = [ "anyhow", "chrono", @@ -6399,6 +6420,7 @@ dependencies = [ "settings", "smol", "strum 0.25.0", + "telemetry_events", "text", "theme", "thiserror", @@ -6460,7 +6482,6 @@ dependencies = [ "async-tar", "async-trait", "collections", - "feature_flags", "futures 0.3.30", "gpui", "http_client", @@ -6537,9 +6558,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.159" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libdbus-sys" @@ -6581,7 +6602,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -6645,18 +6666,18 @@ dependencies = [ [[package]] name = "linkme" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c943daedff228392b791b33bba32e75737756e80a613e32e246c6ce9cbab20a" +checksum = "70fe496a7af8c406f877635cbf3cd6a9fac9d6f443f58691cd8afe6ce0971af4" dependencies = [ "linkme-impl", ] [[package]] name = "linkme-impl" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb26336e6dc7cc76e7927d2c9e7e3bb376d7af65a6f56a0b16c47d18a9b1abc5" +checksum = "b01f197a15988fb5b2ec0a5a9800c97e70771499c456ad757d63b3c5e9b96e75" dependencies = [ "proc-macro2", "quote", @@ -7246,9 +7267,9 @@ dependencies = [ [[package]] name = "nbformat" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146074ad45cab20f5d98ccded164826158471f21d04f96e40b9872529e10979d" +checksum = "84f8a9ab08b34237c2c1d0504b794c2ff01c08dfc46a060d160f004a7f479c31" dependencies = [ "anyhow", "chrono", @@ -8021,9 +8042,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pathdiff" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361" [[package]] name = "pathfinder_geometry" @@ -8898,27 +8919,27 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.87" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] [[package]] name = "profiling" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" dependencies = [ "profiling-procmacros", ] [[package]] name = "profiling-procmacros" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" dependencies = [ "quote", "syn 2.0.76", @@ -9628,9 +9649,11 @@ dependencies = [ "fs", "futures 0.3.30", "gpui", + "itertools 0.13.0", "log", "parking_lot", "prost", + "release_channel", "rpc", "serde", "serde_json", @@ -10060,9 +10083,9 @@ dependencies = [ [[package]] name = "runtimelib" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "263588fe9593333c4bfde258c9021fc64e766ea434e070c6b67c7100536d6499" +checksum = "bc7fe3c17675445fe89de68d130be00b7115104924fbcf53a9b0a84b0283fc81" dependencies = [ "anyhow", "async-dispatcher", @@ -12187,6 +12210,7 @@ dependencies = [ "serde_json_lenient", "serde_repr", "settings", + "strum 0.25.0", "util", "uuid", ] @@ -12217,7 +12241,6 @@ name = "theme_selector" version = "0.1.0" dependencies = [ "client", - "feature_flags", "fs", "fuzzy", "gpui", @@ -14837,7 +14860,6 @@ dependencies = [ "parking_lot", "postage", "project", - "release_channel", "remote", "schemars", "serde", @@ -15159,7 +15181,7 @@ dependencies = [ "diagnostics", "editor", "env_logger 0.11.5", - "extension", + "extension_host", "extensions_ui", "feature_flags", "feedback", diff --git a/Cargo.toml b/Cargo.toml index 4d583f10ca..5abe48f05a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ members = [ "crates/extension", "crates/extension_api", "crates/extension_cli", + "crates/extension_host", "crates/extensions_ui", "crates/feature_flags", "crates/feedback", @@ -210,6 +211,7 @@ debugger_tools = { path = "crates/debugger_tools" } diagnostics = { path = "crates/diagnostics" } editor = { path = "crates/editor" } extension = { path = "crates/extension" } +extension_host = { path = "crates/extension_host" } extensions_ui = { path = "crates/extensions_ui" } feature_flags = { path = "crates/feature_flags" } feedback = { path = "crates/feedback" } @@ -378,7 +380,7 @@ linkify = "0.10.0" log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] } markup5ever_rcdom = "0.3.0" nanoid = "0.4" -nbformat = "0.3.1" +nbformat = "0.3.2" nix = "0.29" num-format = "0.4.4" once_cell = "1.19.0" @@ -411,7 +413,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f "stream", ] } rsa = "0.9.6" -runtimelib = { version = "0.16.0", default-features = false, features = [ +runtimelib = { version = "0.16.1", default-features = false, features = [ "async-dispatcher-runtime", ] } rustc-demangle = "0.1.23" diff --git a/assets/icons/wand.svg b/assets/icons/wand.svg new file mode 100644 index 0000000000..a6704b1c42 --- /dev/null +++ b/assets/icons/wand.svg @@ -0,0 +1 @@ + diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 8b2a728df3..486292f20e 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -157,51 +157,6 @@ "7": ["vim::Number", 7], "8": ["vim::Number", 8], "9": ["vim::Number", 9], - // window related commands (ctrl-w X) - "ctrl-w": null, - "ctrl-w left": ["workspace::ActivatePaneInDirection", "Left"], - "ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"], - "ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"], - "ctrl-w down": ["workspace::ActivatePaneInDirection", "Down"], - "ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"], - "ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"], - "ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"], - "ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"], - "ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"], - "ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"], - "ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"], - "ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"], - "ctrl-w shift-left": ["workspace::SwapPaneInDirection", "Left"], - "ctrl-w shift-right": ["workspace::SwapPaneInDirection", "Right"], - "ctrl-w shift-up": ["workspace::SwapPaneInDirection", "Up"], - "ctrl-w shift-down": ["workspace::SwapPaneInDirection", "Down"], - "ctrl-w shift-h": ["workspace::SwapPaneInDirection", "Left"], - "ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"], - "ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"], - "ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"], - "ctrl-w g t": "pane::ActivateNextItem", - "ctrl-w ctrl-g t": "pane::ActivateNextItem", - "ctrl-w g shift-t": "pane::ActivatePrevItem", - "ctrl-w ctrl-g shift-t": "pane::ActivatePrevItem", - "ctrl-w w": "workspace::ActivateNextPane", - "ctrl-w ctrl-w": "workspace::ActivateNextPane", - "ctrl-w p": "workspace::ActivatePreviousPane", - "ctrl-w ctrl-p": "workspace::ActivatePreviousPane", - "ctrl-w shift-w": "workspace::ActivatePreviousPane", - "ctrl-w ctrl-shift-w": "workspace::ActivatePreviousPane", - "ctrl-w v": "pane::SplitVertical", - "ctrl-w ctrl-v": "pane::SplitVertical", - "ctrl-w s": "pane::SplitHorizontal", - "ctrl-w shift-s": "pane::SplitHorizontal", - "ctrl-w ctrl-s": "pane::SplitHorizontal", - "ctrl-w c": "pane::CloseAllItems", - "ctrl-w ctrl-c": "pane::CloseAllItems", - "ctrl-w q": "pane::CloseAllItems", - "ctrl-w ctrl-q": "pane::CloseAllItems", - "ctrl-w o": "workspace::CloseInactiveTabsAndPanes", - "ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes", - "ctrl-w n": "workspace::NewFileSplitHorizontal", - "ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal", "ctrl-w d": "editor::GoToDefinitionSplit", "ctrl-w g d": "editor::GoToDefinitionSplit", "ctrl-w shift-d": "editor::GoToTypeDefinitionSplit", @@ -339,6 +294,10 @@ "ctrl-t": "vim::Indent", "ctrl-d": "vim::Outdent", "ctrl-k": ["vim::PushOperator", { "Digraph": {} }], + "ctrl-v": ["vim::PushOperator", { "Literal": {} }], + "ctrl-shift-v": "editor::Paste", // note: this is *very* similar to ctrl-v in vim, but ctrl-shift-v on linux is the typical shortcut for paste when ctrl-v is already in use. + "ctrl-q": ["vim::PushOperator", { "Literal": {} }], + "ctrl-shift-q": ["vim::PushOperator", { "Literal": {} }], "ctrl-r": ["vim::PushOperator", "Register"], "insert": "vim::ToggleReplace" } @@ -357,6 +316,10 @@ "ctrl-c": "vim::NormalBefore", "ctrl-[": "vim::NormalBefore", "ctrl-k": ["vim::PushOperator", { "Digraph": {} }], + "ctrl-v": ["vim::PushOperator", { "Literal": {} }], + "ctrl-shift-v": "editor::Paste", // note: this is *very* similar to ctrl-v in vim, but ctrl-shift-v on linux is the typical shortcut for paste when ctrl-v is already in use. + "ctrl-q": ["vim::PushOperator", { "Literal": {} }], + "ctrl-shift-q": ["vim::PushOperator", { "Literal": {} }], "backspace": "vim::UndoReplace", "tab": "vim::Tab", "enter": "vim::Enter", @@ -371,7 +334,9 @@ "escape": "vim::ClearOperators", "ctrl-c": "vim::ClearOperators", "ctrl-[": "vim::ClearOperators", - "ctrl-k": ["vim::PushOperator", { "Digraph": {} }] + "ctrl-k": ["vim::PushOperator", { "Digraph": {} }], + "ctrl-v": ["vim::PushOperator", { "Literal": {} }], + "ctrl-q": ["vim::PushOperator", { "Literal": {} }] } }, { @@ -485,6 +450,49 @@ "c": "vim::CurrentLine" } }, + { + "context": "vim_mode == literal", + "bindings": { + "ctrl-@": ["vim::Literal", ["ctrl-@", "\u0000"]], + "ctrl-a": ["vim::Literal", ["ctrl-a", "\u0001"]], + "ctrl-b": ["vim::Literal", ["ctrl-b", "\u0002"]], + "ctrl-c": ["vim::Literal", ["ctrl-c", "\u0003"]], + "ctrl-d": ["vim::Literal", ["ctrl-d", "\u0004"]], + "ctrl-e": ["vim::Literal", ["ctrl-e", "\u0005"]], + "ctrl-f": ["vim::Literal", ["ctrl-f", "\u0006"]], + "ctrl-g": ["vim::Literal", ["ctrl-g", "\u0007"]], + "ctrl-h": ["vim::Literal", ["ctrl-h", "\u0008"]], + "ctrl-i": ["vim::Literal", ["ctrl-i", "\u0009"]], + "ctrl-j": ["vim::Literal", ["ctrl-j", "\u000A"]], + "ctrl-k": ["vim::Literal", ["ctrl-k", "\u000B"]], + "ctrl-l": ["vim::Literal", ["ctrl-l", "\u000C"]], + "ctrl-m": ["vim::Literal", ["ctrl-m", "\u000D"]], + "ctrl-n": ["vim::Literal", ["ctrl-n", "\u000E"]], + "ctrl-o": ["vim::Literal", ["ctrl-o", "\u000F"]], + "ctrl-p": ["vim::Literal", ["ctrl-p", "\u0010"]], + "ctrl-q": ["vim::Literal", ["ctrl-q", "\u0011"]], + "ctrl-r": ["vim::Literal", ["ctrl-r", "\u0012"]], + "ctrl-s": ["vim::Literal", ["ctrl-s", "\u0013"]], + "ctrl-t": ["vim::Literal", ["ctrl-t", "\u0014"]], + "ctrl-u": ["vim::Literal", ["ctrl-u", "\u0015"]], + "ctrl-v": ["vim::Literal", ["ctrl-v", "\u0016"]], + "ctrl-w": ["vim::Literal", ["ctrl-w", "\u0017"]], + "ctrl-x": ["vim::Literal", ["ctrl-x", "\u0018"]], + "ctrl-y": ["vim::Literal", ["ctrl-y", "\u0019"]], + "ctrl-z": ["vim::Literal", ["ctrl-z", "\u001A"]], + "ctrl-[": ["vim::Literal", ["ctrl-[", "\u001B"]], + "ctrl-\\": ["vim::Literal", ["ctrl-\\", "\u001C"]], + "ctrl-]": ["vim::Literal", ["ctrl-]", "\u001D"]], + "ctrl-^": ["vim::Literal", ["ctrl-^", "\u001E"]], + "ctrl-_": ["vim::Literal", ["ctrl-_", "\u001F"]], + "escape": ["vim::Literal", ["escape", "\u001B"]], + "enter": ["vim::Literal", ["enter", "\u000D"]], + "tab": ["vim::Literal", ["tab", "\u0009"]], + // zed extensions: + "backspace": ["vim::Literal", ["backspace", "\u0008"]], + "delete": ["vim::Literal", ["delete", "\u007F"]] + } + }, { "context": "BufferSearchBar && !in_replace", "bindings": { @@ -493,7 +501,57 @@ } }, { - "context": "EmptyPane || SharedScreen", + "context": "ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView", + "bindings": { + // window related commands (ctrl-w X) + "ctrl-w": null, + "ctrl-w left": ["workspace::ActivatePaneInDirection", "Left"], + "ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"], + "ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"], + "ctrl-w down": ["workspace::ActivatePaneInDirection", "Down"], + "ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"], + "ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"], + "ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"], + "ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"], + "ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"], + "ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"], + "ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"], + "ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"], + "ctrl-w shift-left": ["workspace::SwapPaneInDirection", "Left"], + "ctrl-w shift-right": ["workspace::SwapPaneInDirection", "Right"], + "ctrl-w shift-up": ["workspace::SwapPaneInDirection", "Up"], + "ctrl-w shift-down": ["workspace::SwapPaneInDirection", "Down"], + "ctrl-w shift-h": ["workspace::SwapPaneInDirection", "Left"], + "ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"], + "ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"], + "ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"], + "ctrl-w g t": "pane::ActivateNextItem", + "ctrl-w ctrl-g t": "pane::ActivateNextItem", + "ctrl-w g shift-t": "pane::ActivatePrevItem", + "ctrl-w ctrl-g shift-t": "pane::ActivatePrevItem", + "ctrl-w w": "workspace::ActivateNextPane", + "ctrl-w ctrl-w": "workspace::ActivateNextPane", + "ctrl-w p": "workspace::ActivatePreviousPane", + "ctrl-w ctrl-p": "workspace::ActivatePreviousPane", + "ctrl-w shift-w": "workspace::ActivatePreviousPane", + "ctrl-w ctrl-shift-w": "workspace::ActivatePreviousPane", + "ctrl-w v": "pane::SplitVertical", + "ctrl-w ctrl-v": "pane::SplitVertical", + "ctrl-w s": "pane::SplitHorizontal", + "ctrl-w shift-s": "pane::SplitHorizontal", + "ctrl-w ctrl-s": "pane::SplitHorizontal", + "ctrl-w c": "pane::CloseAllItems", + "ctrl-w ctrl-c": "pane::CloseAllItems", + "ctrl-w q": "pane::CloseAllItems", + "ctrl-w ctrl-q": "pane::CloseAllItems", + "ctrl-w o": "workspace::CloseInactiveTabsAndPanes", + "ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes", + "ctrl-w n": "workspace::NewFileSplitHorizontal", + "ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal" + } + }, + { + "context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView", "bindings": { ":": "command_palette::Toggle", "g /": "pane::DeploySearch" diff --git a/assets/settings/default.json b/assets/settings/default.json index 565e959cf4..bec95445b3 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -652,6 +652,12 @@ // Sets a delay after which the inline blame information is shown. // Delay is restarted with every cursor movement. // "delay_ms": 600 + // + // Whether or not do display the git commit summary on the same line. + // "show_commit_summary": false + // + // The minimum column number to show the inline blame information at + // "min_column": 0 } }, // Configuration for how direnv configuration should be loaded. May take 2 values: diff --git a/assets/settings/initial_tasks.json b/assets/settings/initial_tasks.json index 72f1da0173..31808ac632 100644 --- a/assets/settings/initial_tasks.json +++ b/assets/settings/initial_tasks.json @@ -16,6 +16,7 @@ "allow_concurrent_runs": false, // What to do with the terminal pane and tab, after the command was started: // * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default) + // * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it // * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there "reveal": "always", // What to do with the terminal pane and tab, after the command had finished: diff --git a/crates/activity_indicator/Cargo.toml b/crates/activity_indicator/Cargo.toml index b4fb2ec5b0..6f026d7662 100644 --- a/crates/activity_indicator/Cargo.toml +++ b/crates/activity_indicator/Cargo.toml @@ -16,7 +16,7 @@ doctest = false anyhow.workspace = true auto_update.workspace = true editor.workspace = true -extension.workspace = true +extension_host.workspace = true futures.workspace = true gpui.workspace = true language.workspace = true diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 4959b1192d..f138219da2 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -1,6 +1,6 @@ use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage}; use editor::Editor; -use extension::ExtensionStore; +use extension_host::ExtensionStore; use futures::StreamExt; use gpui::{ actions, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, EventEmitter, @@ -369,7 +369,10 @@ impl ActivityIndicator { .into_any_element(), ), message: format!("Formatting failed: {}. Click to see logs.", failure), - on_click: Some(Arc::new(|_, cx| { + on_click: Some(Arc::new(|indicator, cx| { + indicator.project.update(cx, |project, cx| { + project.reset_last_formatting_failure(cx); + }); cx.dispatch_action(Box::new(workspace::OpenLog)); })), }); diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index c2857d06d4..6e6f7a823e 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -51,6 +51,7 @@ use std::sync::Arc; pub(crate) use streaming_diff::*; use util::ResultExt; +use crate::slash_command::streaming_example_command; use crate::slash_command_settings::SlashCommandSettings; actions!( @@ -468,6 +469,19 @@ fn register_slash_commands(prompt_builder: Option>, cx: &mut }) .detach(); + cx.observe_flag::({ + let slash_command_registry = slash_command_registry.clone(); + move |is_enabled, _cx| { + if is_enabled { + slash_command_registry.register_command( + streaming_example_command::StreamingExampleSlashCommand, + false, + ); + } + } + }) + .detach(); + update_slash_commands_from_settings(cx); cx.observe_global::(update_slash_commands_from_settings) .detach(); diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index eef82c6106..ef647e3033 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -73,12 +73,11 @@ use std::{ }; use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; use text::SelectionGoal; -use ui::TintColor; use ui::{ prelude::*, utils::{format_distance_from_now, DateTimeType}, Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem, - ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tooltip, + ListItemSpacing, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip, }; use util::{maybe, ResultExt}; use workspace::{ @@ -4006,13 +4005,7 @@ impl Render for ContextEditor { } else { None }; - let focus_handle = self - .workspace - .update(cx, |workspace, cx| { - Some(workspace.active_item_as::(cx)?.focus_handle(cx)) - }) - .ok() - .flatten(); + v_flex() .key_context("ContextEditor") .capture_action(cx.listener(ContextEditor::cancel)) @@ -4060,28 +4053,7 @@ impl Render for ContextEditor { .child( h_flex() .gap_1() - .child(render_inject_context_menu(cx.view().downgrade(), cx)) - .child( - IconButton::new("quote-button", IconName::Quote) - .icon_size(IconSize::Small) - .on_click(|_, cx| { - cx.dispatch_action(QuoteSelection.boxed_clone()); - }) - .tooltip(move |cx| { - cx.new_view(|cx| { - Tooltip::new("Insert Selection").key_binding( - focus_handle.as_ref().and_then(|handle| { - KeyBinding::for_action_in( - &QuoteSelection, - &handle, - cx, - ) - }), - ) - }) - .into() - }), - ), + .child(render_inject_context_menu(cx.view().downgrade(), cx)), ) .child( h_flex() @@ -4376,6 +4348,7 @@ fn render_inject_context_menu( Button::new("trigger", "Add Context") .icon(IconName::Plus) .icon_size(IconSize::Small) + .icon_color(Color::Muted) .icon_position(IconPosition::Start) .tooltip(|cx| Tooltip::text("Type / to insert via keyboard", cx)), ) @@ -4550,7 +4523,7 @@ impl Render for ContextEditorToolbarItem { .w_full() .justify_between() .gap_2() - .child(Label::new("Insert Context")) + .child(Label::new("Add Context")) .child(Label::new("/ command").color(Color::Muted)) .into_any() }, @@ -4574,7 +4547,7 @@ impl Render for ContextEditorToolbarItem { } }, ) - .action("Insert Selection", QuoteSelection.boxed_clone()) + .action("Add Selection", QuoteSelection.boxed_clone()) })) } }), diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index a1de9d3b40..5b4cff01b6 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -24,6 +24,7 @@ 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, @@ -1955,6 +1956,7 @@ impl Context { }); match event { + LanguageModelCompletionEvent::StartMessage { .. } => {} LanguageModelCompletionEvent::Stop(reason) => { stop_reason = reason; } @@ -2060,23 +2062,28 @@ impl Context { None }; - if let Some(telemetry) = this.telemetry.as_ref() { - let language_name = this - .buffer - .read(cx) - .language() - .map(|language| language.name()); - telemetry.report_assistant_event(AssistantEvent { + let language_name = this + .buffer + .read(cx) + .language() + .map(|language| language.name()); + report_assistant_event( + AssistantEvent { conversation_id: Some(this.id.0.clone()), kind: AssistantKind::Panel, phase: AssistantPhase::Response, + message_id: None, model: model.telemetry_id(), model_provider: model.provider_id().to_string(), response_latency, error_message, language_name: language_name.map(|name| name.to_proto()), - }); - } + }, + this.telemetry.clone(), + cx.http_client(), + model.api_key(cx), + cx.background_executor(), + ); if let Ok(stop_reason) = result { match stop_reason { @@ -2543,7 +2550,7 @@ impl Context { let mut messages = stream.await?; let mut replaced = !replace_old; - while let Some(message) = messages.next().await { + while let Some(message) = messages.stream.next().await { let text = message?; let mut lines = text.lines(); this.update(&mut cx, |this, cx| { diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index fdf00c8b04..934c2dd5d3 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -21,9 +21,7 @@ use fs::Fs; use futures::{ channel::mpsc, future::{BoxFuture, LocalBoxFuture}, - join, - stream::{self, BoxStream}, - SinkExt, Stream, StreamExt, + join, SinkExt, Stream, StreamExt, }; use gpui::{ anchored, deferred, point, AnyElement, AppContext, ClickEvent, EventEmitter, FocusHandle, @@ -32,7 +30,8 @@ use gpui::{ }; use language::{Buffer, IndentKind, Point, Selection, TransactionId}; use language_model::{ - LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role, + logging::report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest, + LanguageModelRequestMessage, LanguageModelTextStream, Role, }; use multi_buffer::MultiBufferRow; use parking_lot::Mutex; @@ -241,12 +240,13 @@ impl InlineAssistant { }; codegen_ranges.push(start..end); - if let Some(telemetry) = self.telemetry.as_ref() { - if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() { + if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() { + if let Some(telemetry) = self.telemetry.as_ref() { telemetry.report_assistant_event(AssistantEvent { conversation_id: None, kind: AssistantKind::Inline, phase: AssistantPhase::Invoked, + message_id: None, model: model.telemetry_id(), model_provider: model.provider_id().to_string(), response_latency: None, @@ -754,33 +754,6 @@ impl InlineAssistant { pub fn finish_assist(&mut self, assist_id: InlineAssistId, undo: bool, cx: &mut WindowContext) { if let Some(assist) = self.assists.get(&assist_id) { - if let Some(telemetry) = self.telemetry.as_ref() { - if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() { - let language_name = assist.editor.upgrade().and_then(|editor| { - let multibuffer = editor.read(cx).buffer().read(cx); - let ranges = multibuffer.range_to_buffer_ranges(assist.range.clone(), cx); - ranges - .first() - .and_then(|(buffer, _, _)| buffer.read(cx).language()) - .map(|language| language.name()) - }); - telemetry.report_assistant_event(AssistantEvent { - conversation_id: None, - kind: AssistantKind::Inline, - phase: if undo { - AssistantPhase::Rejected - } else { - AssistantPhase::Accepted - }, - model: model.telemetry_id(), - model_provider: model.provider_id().to_string(), - response_latency: None, - error_message: None, - language_name: language_name.map(|name| name.to_proto()), - }); - } - } - let assist_group_id = assist.group_id; if self.assist_groups[&assist_group_id].linked { for assist_id in self.unlink_assist_group(assist_group_id, cx) { @@ -815,12 +788,45 @@ impl InlineAssistant { } } + let active_alternative = assist.codegen.read(cx).active_alternative().clone(); + let message_id = active_alternative.read(cx).message_id.clone(); + + if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() { + let language_name = assist.editor.upgrade().and_then(|editor| { + let multibuffer = editor.read(cx).buffer().read(cx); + let ranges = multibuffer.range_to_buffer_ranges(assist.range.clone(), cx); + ranges + .first() + .and_then(|(buffer, _, _)| buffer.read(cx).language()) + .map(|language| language.name()) + }); + report_assistant_event( + AssistantEvent { + conversation_id: None, + kind: AssistantKind::Inline, + message_id, + phase: if undo { + AssistantPhase::Rejected + } else { + AssistantPhase::Accepted + }, + model: model.telemetry_id(), + model_provider: model.provider_id().to_string(), + response_latency: None, + error_message: None, + language_name: language_name.map(|name| name.to_proto()), + }, + self.telemetry.clone(), + cx.http_client(), + model.api_key(cx), + cx.background_executor(), + ); + } + if undo { assist.codegen.update(cx, |codegen, cx| codegen.undo(cx)); } else { - let confirmed_alternative = assist.codegen.read(cx).active_alternative().clone(); - self.confirmed_assists - .insert(assist_id, confirmed_alternative); + self.confirmed_assists.insert(assist_id, active_alternative); } } } @@ -2497,6 +2503,7 @@ pub struct CodegenAlternative { line_operations: Vec, request: Option, elapsed_time: Option, + message_id: Option, } enum CodegenStatus { @@ -2555,6 +2562,7 @@ impl CodegenAlternative { buffer: buffer.clone(), old_buffer, edit_position: None, + message_id: None, snapshot, last_equal_ranges: Default::default(), transformation_transaction_id: None, @@ -2659,20 +2667,20 @@ impl CodegenAlternative { self.edit_position = Some(self.range.start.bias_right(&self.snapshot)); + let api_key = model.api_key(cx); let telemetry_id = model.telemetry_id(); let provider_id = model.provider_id(); - let chunks: LocalBoxFuture>>> = + let stream: LocalBoxFuture> = if user_prompt.trim().to_lowercase() == "delete" { - async { Ok(stream::empty().boxed()) }.boxed_local() + async { Ok(LanguageModelTextStream::default()) }.boxed_local() } else { let request = self.build_request(user_prompt, assistant_panel_context, cx)?; self.request = Some(request.clone()); - let chunks = cx - .spawn(|_, cx| async move { model.stream_completion_text(request, &cx).await }); - async move { Ok(chunks.await?.boxed()) }.boxed_local() + cx.spawn(|_, cx| async move { model.stream_completion_text(request, &cx).await }) + .boxed_local() }; - self.handle_stream(telemetry_id, provider_id.to_string(), chunks, cx); + self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx); Ok(()) } @@ -2737,7 +2745,8 @@ impl CodegenAlternative { &mut self, model_telemetry_id: String, model_provider_id: String, - stream: impl 'static + Future>>>, + model_api_key: Option, + stream: impl 'static + Future>, cx: &mut ModelContext, ) { let start_time = Instant::now(); @@ -2767,6 +2776,7 @@ impl CodegenAlternative { } } + let http_client = cx.http_client().clone(); let telemetry = self.telemetry.clone(); let language_name = { let multibuffer = self.buffer.read(cx); @@ -2782,15 +2792,21 @@ impl CodegenAlternative { let mut edit_start = self.range.start.to_offset(&snapshot); self.generation = cx.spawn(|codegen, mut cx| { async move { - let chunks = stream.await; + let stream = stream.await; + let message_id = stream + .as_ref() + .ok() + .and_then(|stream| stream.message_id.clone()); let generate = async { let (mut diff_tx, mut diff_rx) = mpsc::channel(1); + let executor = cx.background_executor().clone(); + let message_id = message_id.clone(); let line_based_stream_diff: Task> = cx.background_executor().spawn(async move { let mut response_latency = None; let request_start = Instant::now(); let diff = async { - let chunks = StripInvalidSpans::new(chunks?); + let chunks = StripInvalidSpans::new(stream?.stream); futures::pin_mut!(chunks); let mut diff = StreamingDiff::new(selected_text.to_string()); let mut line_diff = LineDiff::default(); @@ -2886,9 +2902,10 @@ impl CodegenAlternative { let error_message = result.as_ref().err().map(|error| error.to_string()); - if let Some(telemetry) = telemetry { - telemetry.report_assistant_event(AssistantEvent { + report_assistant_event( + AssistantEvent { conversation_id: None, + message_id, kind: AssistantKind::Inline, phase: AssistantPhase::Response, model: model_telemetry_id, @@ -2896,8 +2913,12 @@ impl CodegenAlternative { response_latency, error_message, language_name: language_name.map(|name| name.to_proto()), - }); - } + }, + telemetry, + http_client, + model_api_key, + &executor, + ); result?; Ok(()) @@ -2961,6 +2982,7 @@ impl CodegenAlternative { codegen .update(&mut cx, |this, cx| { + this.message_id = message_id; this.last_equal_ranges.clear(); if let Err(error) = result { this.status = CodegenStatus::Error(error); @@ -3512,15 +3534,7 @@ mod tests { ) }); - let (chunks_tx, chunks_rx) = mpsc::unbounded(); - codegen.update(cx, |codegen, cx| { - codegen.handle_stream( - String::new(), - String::new(), - future::ready(Ok(chunks_rx.map(Ok).boxed())), - cx, - ) - }); + let chunks_tx = simulate_response_stream(codegen.clone(), cx); let mut new_text = concat!( " let mut x = 0;\n", @@ -3584,15 +3598,7 @@ mod tests { ) }); - let (chunks_tx, chunks_rx) = mpsc::unbounded(); - codegen.update(cx, |codegen, cx| { - codegen.handle_stream( - String::new(), - String::new(), - future::ready(Ok(chunks_rx.map(Ok).boxed())), - cx, - ) - }); + let chunks_tx = simulate_response_stream(codegen.clone(), cx); cx.background_executor.run_until_parked(); @@ -3659,15 +3665,7 @@ mod tests { ) }); - let (chunks_tx, chunks_rx) = mpsc::unbounded(); - codegen.update(cx, |codegen, cx| { - codegen.handle_stream( - String::new(), - String::new(), - future::ready(Ok(chunks_rx.map(Ok).boxed())), - cx, - ) - }); + let chunks_tx = simulate_response_stream(codegen.clone(), cx); cx.background_executor.run_until_parked(); @@ -3733,16 +3731,7 @@ mod tests { ) }); - let (chunks_tx, chunks_rx) = mpsc::unbounded(); - codegen.update(cx, |codegen, cx| { - codegen.handle_stream( - String::new(), - String::new(), - future::ready(Ok(chunks_rx.map(Ok).boxed())), - cx, - ) - }); - + let chunks_tx = simulate_response_stream(codegen.clone(), cx); let new_text = concat!( "func main() {\n", "\tx := 0\n", @@ -3797,16 +3786,7 @@ mod tests { ) }); - let (chunks_tx, chunks_rx) = mpsc::unbounded(); - codegen.update(cx, |codegen, cx| { - codegen.handle_stream( - String::new(), - String::new(), - future::ready(Ok(chunks_rx.map(Ok).boxed())), - cx, - ) - }); - + let chunks_tx = simulate_response_stream(codegen.clone(), cx); chunks_tx .unbounded_send("let mut x = 0;\nx += 1;".to_string()) .unwrap(); @@ -3880,6 +3860,26 @@ mod tests { } } + fn simulate_response_stream( + codegen: Model, + cx: &mut TestAppContext, + ) -> mpsc::UnboundedSender { + let (chunks_tx, chunks_rx) = mpsc::unbounded(); + codegen.update(cx, |codegen, cx| { + codegen.handle_stream( + String::new(), + String::new(), + None, + future::ready(Ok(LanguageModelTextStream { + message_id: None, + stream: chunks_rx.map(Ok).boxed(), + })), + cx, + ); + }); + chunks_tx + } + fn rust_lang() -> Language { Language::new( LanguageConfig { diff --git a/crates/assistant/src/slash_command.rs b/crates/assistant/src/slash_command.rs index ed20791d95..2209308081 100644 --- a/crates/assistant/src/slash_command.rs +++ b/crates/assistant/src/slash_command.rs @@ -31,6 +31,7 @@ pub mod now_command; pub mod project_command; pub mod prompt_command; pub mod search_command; +pub mod streaming_example_command; pub mod symbols_command; pub mod tab_command; pub mod terminal_command; diff --git a/crates/assistant/src/slash_command/auto_command.rs b/crates/assistant/src/slash_command/auto_command.rs index cc73f36ebf..61f720be6d 100644 --- a/crates/assistant/src/slash_command/auto_command.rs +++ b/crates/assistant/src/slash_command/auto_command.rs @@ -14,7 +14,7 @@ use language_model::{ use semantic_index::{FileSummary, SemanticDb}; use smol::channel; use std::sync::{atomic::AtomicBool, Arc}; -use ui::{BorrowAppContext, WindowContext}; +use ui::{prelude::*, BorrowAppContext, WindowContext}; use util::ResultExt; use workspace::Workspace; @@ -37,6 +37,10 @@ impl SlashCommand for AutoCommand { "Automatically infer what context to add".into() } + fn icon(&self) -> IconName { + IconName::Wand + } + fn menu_text(&self) -> String { self.description() } diff --git a/crates/assistant/src/slash_command/delta_command.rs b/crates/assistant/src/slash_command/delta_command.rs index c9985d9f00..5c8bc2b023 100644 --- a/crates/assistant/src/slash_command/delta_command.rs +++ b/crates/assistant/src/slash_command/delta_command.rs @@ -10,6 +10,7 @@ use gpui::{Task, WeakView, WindowContext}; use language::{BufferSnapshot, LspAdapterDelegate}; use std::sync::{atomic::AtomicBool, Arc}; use text::OffsetRangeExt; +use ui::prelude::*; use workspace::Workspace; pub(crate) struct DeltaSlashCommand; @@ -27,6 +28,10 @@ impl SlashCommand for DeltaSlashCommand { self.description() } + fn icon(&self) -> IconName { + IconName::Diff + } + fn requires_argument(&self) -> bool { false } diff --git a/crates/assistant/src/slash_command/diagnostics_command.rs b/crates/assistant/src/slash_command/diagnostics_command.rs index c7475445ce..3f1e3e5e71 100644 --- a/crates/assistant/src/slash_command/diagnostics_command.rs +++ b/crates/assistant/src/slash_command/diagnostics_command.rs @@ -98,6 +98,10 @@ impl SlashCommand for DiagnosticsSlashCommand { "Insert diagnostics".into() } + fn icon(&self) -> IconName { + IconName::XCircle + } + fn menu_text(&self) -> String { self.description() } diff --git a/crates/assistant/src/slash_command/file_command.rs b/crates/assistant/src/slash_command/file_command.rs index 1d0fa2bf3e..3964754029 100644 --- a/crates/assistant/src/slash_command/file_command.rs +++ b/crates/assistant/src/slash_command/file_command.rs @@ -117,7 +117,7 @@ impl SlashCommand for FileSlashCommand { } fn description(&self) -> String { - "Insert file".into() + "Insert file and/or directory".into() } fn menu_text(&self) -> String { @@ -128,6 +128,10 @@ impl SlashCommand for FileSlashCommand { true } + fn icon(&self) -> IconName { + IconName::File + } + fn complete_argument( self: Arc, arguments: &[String], diff --git a/crates/assistant/src/slash_command/project_command.rs b/crates/assistant/src/slash_command/project_command.rs index d14cb310ad..ee6434ec03 100644 --- a/crates/assistant/src/slash_command/project_command.rs +++ b/crates/assistant/src/slash_command/project_command.rs @@ -24,7 +24,8 @@ use std::{ ops::DerefMut, sync::{atomic::AtomicBool, Arc}, }; -use ui::{BorrowAppContext as _, IconName}; + +use ui::prelude::*; use workspace::Workspace; pub struct ProjectSlashCommand { @@ -50,6 +51,10 @@ impl SlashCommand for ProjectSlashCommand { "Generate a semantic search based on context".into() } + fn icon(&self) -> IconName { + IconName::Folder + } + fn menu_text(&self) -> String { self.description() } diff --git a/crates/assistant/src/slash_command/prompt_command.rs b/crates/assistant/src/slash_command/prompt_command.rs index 079d1425af..9eb44d3418 100644 --- a/crates/assistant/src/slash_command/prompt_command.rs +++ b/crates/assistant/src/slash_command/prompt_command.rs @@ -21,6 +21,10 @@ impl SlashCommand for PromptSlashCommand { "Insert prompt from library".into() } + fn icon(&self) -> IconName { + IconName::Library + } + fn menu_text(&self) -> String { self.description() } diff --git a/crates/assistant/src/slash_command/search_command.rs b/crates/assistant/src/slash_command/search_command.rs index 9c4938ce93..f4bc3e36b6 100644 --- a/crates/assistant/src/slash_command/search_command.rs +++ b/crates/assistant/src/slash_command/search_command.rs @@ -38,6 +38,10 @@ impl SlashCommand for SearchSlashCommand { "Search your project semantically".into() } + fn icon(&self) -> IconName { + IconName::SearchCode + } + fn menu_text(&self) -> String { self.description() } diff --git a/crates/assistant/src/slash_command/streaming_example_command.rs b/crates/assistant/src/slash_command/streaming_example_command.rs new file mode 100644 index 0000000000..ae805669d2 --- /dev/null +++ b/crates/assistant/src/slash_command/streaming_example_command.rs @@ -0,0 +1,136 @@ +use std::sync::atomic::AtomicBool; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Result; +use assistant_slash_command::{ + ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent, + SlashCommandOutputSection, SlashCommandResult, +}; +use feature_flags::FeatureFlag; +use futures::channel::mpsc; +use gpui::{Task, WeakView}; +use language::{BufferSnapshot, LspAdapterDelegate}; +use smol::stream::StreamExt; +use smol::Timer; +use ui::prelude::*; +use workspace::Workspace; + +pub struct StreamingExampleSlashCommandFeatureFlag; + +impl FeatureFlag for StreamingExampleSlashCommandFeatureFlag { + const NAME: &'static str = "streaming-example-slash-command"; +} + +pub(crate) struct StreamingExampleSlashCommand; + +impl SlashCommand for StreamingExampleSlashCommand { + fn name(&self) -> String { + "streaming-example".into() + } + + fn description(&self) -> String { + "An example slash command that showcases streaming.".into() + } + + fn menu_text(&self) -> String { + self.description() + } + + fn requires_argument(&self) -> bool { + false + } + + fn complete_argument( + self: Arc, + _arguments: &[String], + _cancel: Arc, + _workspace: Option>, + _cx: &mut WindowContext, + ) -> Task>> { + Task::ready(Ok(Vec::new())) + } + + fn run( + self: Arc, + _arguments: &[String], + _context_slash_command_output_sections: &[SlashCommandOutputSection], + _context_buffer: BufferSnapshot, + _workspace: WeakView, + _delegate: Option>, + cx: &mut WindowContext, + ) -> Task { + let (events_tx, events_rx) = mpsc::unbounded(); + cx.background_executor() + .spawn(async move { + events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection { + icon: IconName::FileRust, + label: "Section 1".into(), + metadata: None, + }))?; + events_tx.unbounded_send(Ok(SlashCommandEvent::Content( + SlashCommandContent::Text { + text: "Hello".into(), + run_commands_in_text: false, + }, + )))?; + events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?; + events_tx.unbounded_send(Ok(SlashCommandEvent::Content( + SlashCommandContent::Text { + text: "\n".into(), + run_commands_in_text: false, + }, + )))?; + + Timer::after(Duration::from_secs(1)).await; + + events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection { + icon: IconName::FileRust, + label: "Section 2".into(), + metadata: None, + }))?; + events_tx.unbounded_send(Ok(SlashCommandEvent::Content( + SlashCommandContent::Text { + text: "World".into(), + run_commands_in_text: false, + }, + )))?; + events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?; + events_tx.unbounded_send(Ok(SlashCommandEvent::Content( + SlashCommandContent::Text { + text: "\n".into(), + run_commands_in_text: false, + }, + )))?; + + for n in 1..=10 { + Timer::after(Duration::from_secs(1)).await; + + events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection { + icon: IconName::StarFilled, + label: format!("Section {n}").into(), + metadata: None, + }))?; + events_tx.unbounded_send(Ok(SlashCommandEvent::Content( + SlashCommandContent::Text { + text: "lorem ipsum ".repeat(n).trim().into(), + run_commands_in_text: false, + }, + )))?; + events_tx + .unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?; + events_tx.unbounded_send(Ok(SlashCommandEvent::Content( + SlashCommandContent::Text { + text: "\n".into(), + run_commands_in_text: false, + }, + )))?; + } + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + + Task::ready(Ok(events_rx.boxed())) + } +} diff --git a/crates/assistant/src/slash_command/symbols_command.rs b/crates/assistant/src/slash_command/symbols_command.rs index 468c8d7126..2b261bc368 100644 --- a/crates/assistant/src/slash_command/symbols_command.rs +++ b/crates/assistant/src/slash_command/symbols_command.rs @@ -22,6 +22,10 @@ impl SlashCommand for OutlineSlashCommand { "Insert symbols for active tab".into() } + fn icon(&self) -> IconName { + IconName::ListTree + } + fn menu_text(&self) -> String { self.description() } diff --git a/crates/assistant/src/slash_command/tab_command.rs b/crates/assistant/src/slash_command/tab_command.rs index 771c0765ee..1579938c92 100644 --- a/crates/assistant/src/slash_command/tab_command.rs +++ b/crates/assistant/src/slash_command/tab_command.rs @@ -12,7 +12,7 @@ use std::{ path::PathBuf, sync::{atomic::AtomicBool, Arc}, }; -use ui::{ActiveTheme, WindowContext}; +use ui::{prelude::*, ActiveTheme, WindowContext}; use util::ResultExt; use workspace::Workspace; @@ -31,6 +31,10 @@ impl SlashCommand for TabSlashCommand { "Insert open tabs (active tab by default)".to_owned() } + fn icon(&self) -> IconName { + IconName::FileTree + } + fn menu_text(&self) -> String { self.description() } diff --git a/crates/assistant/src/slash_command/terminal_command.rs b/crates/assistant/src/slash_command/terminal_command.rs index 2ca1d4041b..84dbb7146f 100644 --- a/crates/assistant/src/slash_command/terminal_command.rs +++ b/crates/assistant/src/slash_command/terminal_command.rs @@ -33,6 +33,10 @@ impl SlashCommand for TerminalSlashCommand { "Insert terminal output".into() } + fn icon(&self) -> IconName { + IconName::Terminal + } + fn menu_text(&self) -> String { self.description() } diff --git a/crates/assistant/src/slash_command_picker.rs b/crates/assistant/src/slash_command_picker.rs index 35ae90d412..3d667d7f82 100644 --- a/crates/assistant/src/slash_command_picker.rs +++ b/crates/assistant/src/slash_command_picker.rs @@ -1,19 +1,13 @@ use std::sync::Arc; use assistant_slash_command::SlashCommandRegistry; -use gpui::AnyElement; -use gpui::DismissEvent; -use gpui::WeakView; -use picker::PickerEditorPosition; -use ui::ListItemSpacing; - -use gpui::SharedString; -use gpui::Task; -use picker::{Picker, PickerDelegate}; -use ui::{prelude::*, ListItem, PopoverMenu, PopoverTrigger}; +use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakView}; +use picker::{Picker, PickerDelegate, PickerEditorPosition}; +use ui::{prelude::*, KeyBinding, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger}; use crate::assistant_panel::ContextEditor; +use crate::QuoteSelection; #[derive(IntoElement)] pub(super) struct SlashCommandSelector { @@ -27,6 +21,7 @@ struct SlashCommandInfo { name: SharedString, description: SharedString, args: Option, + icon: IconName, } #[derive(Clone)] @@ -37,6 +32,7 @@ enum SlashCommandEntry { renderer: fn(&mut WindowContext<'_>) -> AnyElement, on_confirm: fn(&mut WindowContext<'_>), }, + QuoteButton, } impl AsRef for SlashCommandEntry { @@ -44,6 +40,7 @@ impl AsRef for SlashCommandEntry { match self { SlashCommandEntry::Info(SlashCommandInfo { name, .. }) | SlashCommandEntry::Advert { name, .. } => name, + SlashCommandEntry::QuoteButton => "Quote Selection", } } } @@ -145,16 +142,23 @@ impl PickerDelegate for SlashCommandDelegate { } ret } + fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext>) { if let Some(command) = self.filtered_commands.get(self.selected_index) { - if let SlashCommandEntry::Info(info) = command { - self.active_context_editor - .update(cx, |context_editor, cx| { - context_editor.insert_command(&info.name, cx) - }) - .ok(); - } else if let SlashCommandEntry::Advert { on_confirm, .. } = command { - on_confirm(cx); + match command { + SlashCommandEntry::Info(info) => { + self.active_context_editor + .update(cx, |context_editor, cx| { + context_editor.insert_command(&info.name, cx) + }) + .ok(); + } + SlashCommandEntry::QuoteButton => { + cx.dispatch_action(Box::new(QuoteSelection)); + } + SlashCommandEntry::Advert { on_confirm, .. } => { + on_confirm(cx); + } } cx.emit(DismissEvent); } @@ -181,46 +185,78 @@ impl PickerDelegate for SlashCommandDelegate { .spacing(ListItemSpacing::Dense) .selected(selected) .child( - h_flex() + v_flex() .group(format!("command-entry-label-{ix}")) .w_full() .min_w(px(250.)) .child( - v_flex() - .child( - h_flex() - .child(div().font_buffer(cx).child({ - let mut label = format!("/{}", info.name); - if let Some(args) = - info.args.as_ref().filter(|_| selected) - { - label.push_str(&args); - } - Label::new(label).size(LabelSize::Small) - })) - .children(info.args.clone().filter(|_| !selected).map( - |args| { - div() - .font_buffer(cx) - .child( - Label::new(args) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .visible_on_hover(format!( - "command-entry-label-{ix}" - )) - }, - )), - ) - .child( - Label::new(info.description.clone()) - .size(LabelSize::Small) - .color(Color::Muted), - ), + h_flex() + .gap_1p5() + .child(Icon::new(info.icon).size(IconSize::XSmall)) + .child(div().font_buffer(cx).child({ + let mut label = format!("{}", info.name); + if let Some(args) = info.args.as_ref().filter(|_| selected) + { + label.push_str(&args); + } + Label::new(label).size(LabelSize::Small) + })) + .children(info.args.clone().filter(|_| !selected).map( + |args| { + div() + .font_buffer(cx) + .child( + Label::new(args) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .visible_on_hover(format!( + "command-entry-label-{ix}" + )) + }, + )), + ) + .child( + Label::new(info.description.clone()) + .size(LabelSize::Small) + .color(Color::Muted), ), ), ), + SlashCommandEntry::QuoteButton => { + let focus = cx.focus_handle(); + let key_binding = KeyBinding::for_action_in(&QuoteSelection, &focus, cx); + + Some( + ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Dense) + .selected(selected) + .child( + v_flex() + .child( + h_flex() + .gap_1p5() + .child(Icon::new(IconName::Quote).size(IconSize::XSmall)) + .child( + div().font_buffer(cx).child( + Label::new("selection").size(LabelSize::Small), + ), + ), + ) + .child( + h_flex() + .gap_1p5() + .child( + Label::new("Insert editor selection") + .color(Color::Muted) + .size(LabelSize::Small), + ) + .children(key_binding.map(|kb| kb.render(cx))), + ), + ), + ) + } SlashCommandEntry::Advert { renderer, .. } => Some( ListItem::new(ix) .inset(true) @@ -251,31 +287,50 @@ impl RenderOnce for SlashCommandSelector { name: command_name.into(), description: menu_text, args, + icon: command.icon(), })) }) - .chain([SlashCommandEntry::Advert { - name: "create-your-command".into(), - renderer: |cx| { - v_flex() - .child( - h_flex() - .font_buffer(cx) - .items_center() - .gap_1() - .child(div().font_buffer(cx).child( - Label::new("create-your-command").size(LabelSize::Small), - )) - .child(Icon::new(IconName::ArrowUpRight).size(IconSize::XSmall)), - ) - .child( - Label::new("Learn how to create a custom command") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .into_any_element() + .chain([ + SlashCommandEntry::Advert { + name: "create-your-command".into(), + renderer: |cx| { + v_flex() + .w_full() + .child( + h_flex() + .w_full() + .font_buffer(cx) + .items_center() + .justify_between() + .child( + h_flex() + .items_center() + .gap_1p5() + .child(Icon::new(IconName::Plus).size(IconSize::XSmall)) + .child( + div().font_buffer(cx).child( + Label::new("create-your-command") + .size(LabelSize::Small), + ), + ), + ) + .child( + Icon::new(IconName::ArrowUpRight) + .size(IconSize::XSmall) + .color(Color::Muted), + ), + ) + .child( + Label::new("Create your custom command") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .into_any_element() + }, + on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"), }, - on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"), - }]) + SlashCommandEntry::QuoteButton, + ]) .collect::>(); let delegate = SlashCommandDelegate { diff --git a/crates/assistant/src/terminal_inline_assistant.rs b/crates/assistant/src/terminal_inline_assistant.rs index 3e472ae4a9..2fb4b4ffda 100644 --- a/crates/assistant/src/terminal_inline_assistant.rs +++ b/crates/assistant/src/terminal_inline_assistant.rs @@ -17,7 +17,8 @@ use gpui::{ }; use language::Buffer; use language_model::{ - LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role, + logging::report_assistant_event, LanguageModelRegistry, LanguageModelRequest, + LanguageModelRequestMessage, Role, }; use settings::Settings; use std::{ @@ -306,6 +307,33 @@ impl TerminalInlineAssistant { this.focus_handle(cx).focus(cx); }) .log_err(); + + if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() { + let codegen = assist.codegen.read(cx); + let executor = cx.background_executor().clone(); + report_assistant_event( + AssistantEvent { + conversation_id: None, + kind: AssistantKind::InlineTerminal, + message_id: codegen.message_id.clone(), + phase: if undo { + AssistantPhase::Rejected + } else { + AssistantPhase::Accepted + }, + model: model.telemetry_id(), + model_provider: model.provider_id().to_string(), + response_latency: None, + error_message: None, + language_name: None, + }, + codegen.telemetry.clone(), + cx.http_client(), + model.api_key(cx), + &executor, + ); + } + assist.codegen.update(cx, |codegen, cx| { if undo { codegen.undo(cx); @@ -1016,6 +1044,7 @@ pub struct Codegen { telemetry: Option>, terminal: Model, generation: Task<()>, + message_id: Option, transaction: Option, } @@ -1026,6 +1055,7 @@ impl Codegen { telemetry, status: CodegenStatus::Idle, generation: Task::ready(()), + message_id: None, transaction: None, } } @@ -1035,6 +1065,8 @@ impl Codegen { return; }; + let model_api_key = model.api_key(cx); + let http_client = cx.http_client(); let telemetry = self.telemetry.clone(); self.status = CodegenStatus::Pending; self.transaction = Some(TerminalTransaction::start(self.terminal.clone())); @@ -1043,44 +1075,62 @@ impl Codegen { let model_provider_id = model.provider_id(); let response = model.stream_completion_text(prompt, &cx).await; let generate = async { + let message_id = response + .as_ref() + .ok() + .and_then(|response| response.message_id.clone()); + let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); - let task = cx.background_executor().spawn(async move { - let mut response_latency = None; - let request_start = Instant::now(); - let task = async { - let mut chunks = response?; - while let Some(chunk) = chunks.next().await { - if response_latency.is_none() { - response_latency = Some(request_start.elapsed()); + let task = cx.background_executor().spawn({ + let message_id = message_id.clone(); + let executor = cx.background_executor().clone(); + async move { + let mut response_latency = None; + let request_start = Instant::now(); + let task = async { + let mut chunks = response?.stream; + while let Some(chunk) = chunks.next().await { + if response_latency.is_none() { + response_latency = Some(request_start.elapsed()); + } + let chunk = chunk?; + hunks_tx.send(chunk).await?; } - let chunk = chunk?; - hunks_tx.send(chunk).await?; - } + anyhow::Ok(()) + }; + + let result = task.await; + + let error_message = result.as_ref().err().map(|error| error.to_string()); + report_assistant_event( + AssistantEvent { + conversation_id: None, + kind: AssistantKind::InlineTerminal, + message_id, + phase: AssistantPhase::Response, + model: model_telemetry_id, + model_provider: model_provider_id.to_string(), + response_latency, + error_message, + language_name: None, + }, + telemetry, + http_client, + model_api_key, + &executor, + ); + + result?; anyhow::Ok(()) - }; - - let result = task.await; - - let error_message = result.as_ref().err().map(|error| error.to_string()); - if let Some(telemetry) = telemetry { - telemetry.report_assistant_event(AssistantEvent { - conversation_id: None, - kind: AssistantKind::Inline, - phase: AssistantPhase::Response, - model: model_telemetry_id, - model_provider: model_provider_id.to_string(), - response_latency, - error_message, - language_name: None, - }); } - - result?; - anyhow::Ok(()) }); + this.update(&mut cx, |this, _| { + this.message_id = message_id; + })?; + while let Some(hunk) = hunks_rx.next().await { this.update(&mut cx, |this, cx| { if let Some(transaction) = &mut this.transaction { diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index de247602d8..58f4fcb9b4 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -62,6 +62,9 @@ pub type SlashCommandResult = Result String; + fn icon(&self) -> IconName { + IconName::Slash + } fn label(&self, _cx: &AppContext) -> CodeLabel { CodeLabel::plain(self.name(), None) } diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index fbbd23907a..be0c1b40a3 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -686,6 +686,12 @@ async fn download_remote_server_binary( let request_body = AsyncBody::from(serde_json::to_string(&update_request_body)?); let mut response = client.get(&release.url, request_body, true).await?; + if !response.status().is_success() { + return Err(anyhow!( + "failed to download remote server release: {:?}", + response.status() + )); + } smol::io::copy(response.body_mut(), &mut temp_file).await?; smol::fs::rename(&temp, &target_path).await?; diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index 468d381258..974c860c08 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -13,7 +13,6 @@ path = "src/call.rs" doctest = false [features] -no-webrtc = ["live_kit_client/no-webrtc"] test-support = [ "client/test-support", "collections/test-support", diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index ba03255d54..25f8709ff1 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -341,6 +341,13 @@ impl Telemetry { .detach(); } + pub fn metrics_enabled(self: &Arc) -> bool { + let state = self.state.lock(); + let enabled = state.settings.metrics; + drop(state); + return enabled; + } + pub fn set_authenticated_user_info( self: &Arc, metrics_id: Option, diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 1daffffb4e..ffb7414d3d 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -986,6 +986,7 @@ fn editor_blocks( em_width: px(0.), max_width: px(0.), block_id, + selected: false, editor_style: &editor::EditorStyle::default(), }); let element = element.downcast_mut::>().unwrap(); diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 8e7c7358cf..3b660a252f 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -80,6 +80,8 @@ pub struct ConfirmCodeAction { pub struct ToggleComments { #[serde(default)] pub advance_downwards: bool, + #[serde(default)] + pub ignore_indent: bool, } #[derive(PartialEq, Clone, Deserialize, Default)] @@ -157,6 +159,13 @@ pub struct DeleteToPreviousWordStart { pub struct FoldAtLevel { pub level: u32, } + +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct SpawnNearestTask { + #[serde(default)] + pub reveal: task::RevealStrategy, +} + impl_actions!( editor, [ @@ -182,6 +191,7 @@ impl_actions!( SelectToBeginningOfLine, SelectToEndOfLine, SelectUpByLines, + SpawnNearestTask, ShowCompletions, ToggleCodeActions, ToggleComments, diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 18ea4db838..2be02927ea 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -660,7 +660,7 @@ impl DisplaySnapshot { new_start..new_end } - fn point_to_display_point(&self, point: MultiBufferPoint, bias: Bias) -> DisplayPoint { + pub fn point_to_display_point(&self, point: MultiBufferPoint, bias: Bias) -> DisplayPoint { let inlay_point = self.inlay_snapshot.to_inlay_point(point); let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias); let tab_point = self.tab_snapshot.to_tab_point(fold_point); @@ -669,7 +669,7 @@ impl DisplaySnapshot { DisplayPoint(block_point) } - fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point { + pub fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point { self.inlay_snapshot .to_buffer_point(self.display_point_to_inlay_point(point, bias)) } @@ -702,7 +702,7 @@ impl DisplaySnapshot { fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint { let block_point = point.0; - let wrap_point = self.block_snapshot.to_wrap_point(block_point); + let wrap_point = self.block_snapshot.to_wrap_point(block_point, bias); let tab_point = self.wrap_snapshot.to_tab_point(wrap_point); let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0; fold_point.to_inlay_point(&self.fold_snapshot) @@ -710,7 +710,7 @@ impl DisplaySnapshot { pub fn display_point_to_fold_point(&self, point: DisplayPoint, bias: Bias) -> FoldPoint { let block_point = point.0; - let wrap_point = self.block_snapshot.to_wrap_point(block_point); + let wrap_point = self.block_snapshot.to_wrap_point(block_point, bias); let tab_point = self.wrap_snapshot.to_tab_point(wrap_point); self.tab_snapshot.to_fold_point(tab_point, bias).0 } @@ -1001,7 +1001,7 @@ impl DisplaySnapshot { pub fn soft_wrap_indent(&self, display_row: DisplayRow) -> Option { let wrap_row = self .block_snapshot - .to_wrap_point(BlockPoint::new(display_row.0, 0)) + .to_wrap_point(BlockPoint::new(display_row.0, 0), Bias::Left) .row(); self.wrap_snapshot.soft_wrap_indent(wrap_row) } @@ -1233,7 +1233,7 @@ impl DisplayPoint { } pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> usize { - let wrap_point = map.block_snapshot.to_wrap_point(self.0); + let wrap_point = map.block_snapshot.to_wrap_point(self.0, bias); let tab_point = map.wrap_snapshot.to_tab_point(wrap_point); let fold_point = map.tab_snapshot.to_fold_point(tab_point, bias).0; let inlay_point = fold_point.to_inlay_point(&map.fold_snapshot); @@ -2059,6 +2059,112 @@ pub mod tests { ); } + #[gpui::test] + async fn test_point_translation_with_replace_blocks(cx: &mut gpui::TestAppContext) { + cx.background_executor + .set_block_on_ticks(usize::MAX..=usize::MAX); + + cx.update(|cx| init_test(cx, |_| {})); + + let buffer = cx.update(|cx| MultiBuffer::build_simple("abcde\nfghij\nklmno\npqrst", cx)); + let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); + let map = cx.new_model(|cx| { + DisplayMap::new( + buffer.clone(), + font("Courier"), + px(16.0), + None, + true, + 1, + 1, + 0, + FoldPlaceholder::test(), + cx, + ) + }); + + let snapshot = map.update(cx, |map, cx| { + map.insert_blocks( + [BlockProperties { + placement: BlockPlacement::Replace( + buffer_snapshot.anchor_before(Point::new(1, 2)) + ..buffer_snapshot.anchor_after(Point::new(2, 3)), + ), + height: 4, + style: BlockStyle::Fixed, + render: Box::new(|_| div().into_any()), + priority: 0, + }], + cx, + ); + map.snapshot(cx) + }); + + assert_eq!(snapshot.text(), "abcde\n\n\n\n\npqrst"); + + let point_to_display_points = [ + (Point::new(1, 0), DisplayPoint::new(DisplayRow(1), 0)), + (Point::new(2, 0), DisplayPoint::new(DisplayRow(1), 0)), + (Point::new(3, 0), DisplayPoint::new(DisplayRow(5), 0)), + ]; + for (buffer_point, display_point) in point_to_display_points { + assert_eq!( + snapshot.point_to_display_point(buffer_point, Bias::Left), + display_point, + "point_to_display_point({:?}, Bias::Left)", + buffer_point + ); + assert_eq!( + snapshot.point_to_display_point(buffer_point, Bias::Right), + display_point, + "point_to_display_point({:?}, Bias::Right)", + buffer_point + ); + } + + let display_points_to_points = [ + ( + DisplayPoint::new(DisplayRow(1), 0), + Point::new(1, 0), + Point::new(2, 5), + ), + ( + DisplayPoint::new(DisplayRow(2), 0), + Point::new(1, 0), + Point::new(2, 5), + ), + ( + DisplayPoint::new(DisplayRow(3), 0), + Point::new(1, 0), + Point::new(2, 5), + ), + ( + DisplayPoint::new(DisplayRow(4), 0), + Point::new(1, 0), + Point::new(2, 5), + ), + ( + DisplayPoint::new(DisplayRow(5), 0), + Point::new(3, 0), + Point::new(3, 0), + ), + ]; + for (display_point, left_buffer_point, right_buffer_point) in display_points_to_points { + assert_eq!( + snapshot.display_point_to_point(display_point, Bias::Left), + left_buffer_point, + "display_point_to_point({:?}, Bias::Left)", + display_point + ); + assert_eq!( + snapshot.display_point_to_point(display_point, Bias::Right), + right_buffer_point, + "display_point_to_point({:?}, Bias::Right)", + display_point + ); + } + } + // todo(linux) fails due to pixel differences in text rendering #[cfg(target_os = "macos")] #[gpui::test] diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 4d249d1ed5..5b8dbbd896 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -265,6 +265,7 @@ pub struct BlockContext<'a, 'b> { pub em_width: Pixels, pub line_height: Pixels, pub block_id: BlockId, + pub selected: bool, pub editor_style: &'b EditorStyle, } @@ -1311,7 +1312,6 @@ impl BlockSnapshot { let (output_start_row, input_start_row) = cursor.start(); let (output_end_row, input_end_row) = cursor.end(&()); let output_start = Point::new(output_start_row.0, 0); - let output_end = Point::new(output_end_row.0, 0); let input_start = Point::new(input_start_row.0, 0); let input_end = Point::new(input_end_row.0, 0); @@ -1319,10 +1319,10 @@ impl BlockSnapshot { Some(Block::Custom(block)) if matches!(block.placement, BlockPlacement::Replace(_)) => { - if bias == Bias::Left { + if ((bias == Bias::Left || search_left) && output_start <= point.0) + || (!search_left && output_start >= point.0) + { return BlockPoint(output_start); - } else { - return BlockPoint(Point::new(output_end.row - 1, 0)); } } None => { @@ -1364,12 +1364,7 @@ impl BlockSnapshot { cursor.seek(&WrapRow(wrap_point.row()), Bias::Right, &()); if let Some(transform) = cursor.item() { if transform.block.is_some() { - let wrap_start = WrapPoint::new(cursor.start().0 .0, 0); - if wrap_start == wrap_point { - BlockPoint::new(cursor.start().1 .0, 0) - } else { - BlockPoint::new(cursor.end(&()).1 .0 - 1, 0) - } + BlockPoint::new(cursor.start().1 .0, 0) } else { let (input_start_row, output_start_row) = cursor.start(); let input_start = Point::new(input_start_row.0, 0); @@ -1382,7 +1377,7 @@ impl BlockSnapshot { } } - pub fn to_wrap_point(&self, block_point: BlockPoint) -> WrapPoint { + pub fn to_wrap_point(&self, block_point: BlockPoint, bias: Bias) -> WrapPoint { let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); cursor.seek(&BlockRow(block_point.row), Bias::Right, &()); if let Some(transform) = cursor.item() { @@ -1391,7 +1386,9 @@ impl BlockSnapshot { if block.place_below() { let wrap_row = cursor.start().1 .0 - 1; WrapPoint::new(wrap_row, self.wrap_snapshot.line_len(wrap_row)) - } else if block.place_above() || block_point.row == cursor.start().0 .0 { + } else if block.place_above() { + WrapPoint::new(cursor.start().1 .0, 0) + } else if bias == Bias::Left { WrapPoint::new(cursor.start().1 .0, 0) } else { let wrap_row = cursor.end(&()).1 .0 - 1; @@ -1766,19 +1763,19 @@ mod tests { ); assert_eq!( - snapshot.to_wrap_point(BlockPoint::new(0, 3)), + snapshot.to_wrap_point(BlockPoint::new(0, 3), Bias::Left), WrapPoint::new(0, 3) ); assert_eq!( - snapshot.to_wrap_point(BlockPoint::new(1, 0)), + snapshot.to_wrap_point(BlockPoint::new(1, 0), Bias::Left), WrapPoint::new(1, 0) ); assert_eq!( - snapshot.to_wrap_point(BlockPoint::new(3, 0)), + snapshot.to_wrap_point(BlockPoint::new(3, 0), Bias::Left), WrapPoint::new(1, 0) ); assert_eq!( - snapshot.to_wrap_point(BlockPoint::new(7, 0)), + snapshot.to_wrap_point(BlockPoint::new(7, 0), Bias::Left), WrapPoint::new(3, 3) ); @@ -2616,10 +2613,15 @@ mod tests { // Ensure that conversion between block points and wrap points is stable. for row in 0..=blocks_snapshot.wrap_snapshot.max_point().row() { - let original_wrap_point = WrapPoint::new(row, 0); - let block_point = blocks_snapshot.to_block_point(original_wrap_point); - let wrap_point = blocks_snapshot.to_wrap_point(block_point); - assert_eq!(blocks_snapshot.to_block_point(wrap_point), block_point); + let wrap_point = WrapPoint::new(row, 0); + let block_point = blocks_snapshot.to_block_point(wrap_point); + let left_wrap_point = blocks_snapshot.to_wrap_point(block_point, Bias::Left); + let right_wrap_point = blocks_snapshot.to_wrap_point(block_point, Bias::Right); + assert_eq!(blocks_snapshot.to_block_point(left_wrap_point), block_point); + assert_eq!( + blocks_snapshot.to_block_point(right_wrap_point), + block_point + ); } let mut block_point = BlockPoint::new(0, 0); @@ -2627,10 +2629,12 @@ mod tests { let left_point = blocks_snapshot.clip_point(block_point, Bias::Left); let left_buffer_point = blocks_snapshot.to_point(left_point, Bias::Left); assert_eq!( - blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(left_point)), + blocks_snapshot + .to_block_point(blocks_snapshot.to_wrap_point(left_point, Bias::Left)), left_point, - "wrap point: {:?}", - blocks_snapshot.to_wrap_point(left_point) + "block point: {:?}, wrap point: {:?}", + block_point, + blocks_snapshot.to_wrap_point(left_point, Bias::Left) ); assert_eq!( left_buffer_point, @@ -2642,10 +2646,12 @@ mod tests { let right_point = blocks_snapshot.clip_point(block_point, Bias::Right); let right_buffer_point = blocks_snapshot.to_point(right_point, Bias::Right); assert_eq!( - blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(right_point)), + blocks_snapshot + .to_block_point(blocks_snapshot.to_wrap_point(right_point, Bias::Right)), right_point, - "wrap point: {:?}", - blocks_snapshot.to_wrap_point(right_point) + "block point: {:?}, wrap point: {:?}", + block_point, + blocks_snapshot.to_wrap_point(right_point, Bias::Right) ); assert_eq!( right_buffer_point, @@ -2681,7 +2687,8 @@ mod tests { impl BlockSnapshot { fn to_point(&self, point: BlockPoint, bias: Bias) -> Point { - self.wrap_snapshot.to_point(self.to_wrap_point(point), bias) + self.wrap_snapshot + .to_point(self.to_wrap_point(point, bias), bias) } } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1962cf3fbd..a040ba5f9e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -133,7 +133,9 @@ use project::{ use rand::prelude::*; use rpc::{proto::*, ErrorExt}; use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide}; -use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection}; +use selections_collection::{ + resolve_selections, MutableSelectionsCollection, SelectionsCollection, +}; use serde::{Deserialize, Serialize}; use settings::{update_settings_file, Settings, SettingsLocation, SettingsStore}; use smallvec::SmallVec; @@ -505,6 +507,19 @@ struct RunnableTasks { context_range: Range, } +impl RunnableTasks { + fn resolve<'a>( + &'a self, + cx: &'a task::TaskContext, + ) -> impl Iterator + 'a { + self.templates.iter().filter_map(|(kind, template)| { + template + .resolve_task(&kind.to_id_base(), cx) + .map(|task| (kind.clone(), task)) + }) + } +} + #[derive(Clone)] struct ResolvedTasks { templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>, @@ -3489,8 +3504,8 @@ impl Editor { } let new_anchor_selections = new_selections.iter().map(|e| &e.0); let new_selection_deltas = new_selections.iter().map(|e| e.1); - let snapshot = this.buffer.read(cx).read(cx); - let new_selections = resolve_multiple::(new_anchor_selections, &snapshot) + let map = this.display_map.update(cx, |map, cx| map.snapshot(cx)); + let new_selections = resolve_selections::(new_anchor_selections, &map) .zip(new_selection_deltas) .map(|(selection, delta)| Selection { id: selection.id, @@ -3503,18 +3518,20 @@ impl Editor { let mut i = 0; for (position, delta, selection_id, pair) in new_autoclose_regions { - let position = position.to_offset(&snapshot) + delta; - let start = snapshot.anchor_before(position); - let end = snapshot.anchor_after(position); + let position = position.to_offset(&map.buffer_snapshot) + delta; + let start = map.buffer_snapshot.anchor_before(position); + let end = map.buffer_snapshot.anchor_after(position); while let Some(existing_state) = this.autoclose_regions.get(i) { - match existing_state.range.start.cmp(&start, &snapshot) { + match existing_state.range.start.cmp(&start, &map.buffer_snapshot) { Ordering::Less => i += 1, Ordering::Greater => break, - Ordering::Equal => match end.cmp(&existing_state.range.end, &snapshot) { - Ordering::Less => i += 1, - Ordering::Equal => break, - Ordering::Greater => break, - }, + Ordering::Equal => { + match end.cmp(&existing_state.range.end, &map.buffer_snapshot) { + Ordering::Less => i += 1, + Ordering::Equal => break, + Ordering::Greater => break, + } + } } } this.autoclose_regions.insert( @@ -3527,7 +3544,6 @@ impl Editor { ); } - drop(snapshot); let had_active_inline_completion = this.has_active_inline_completion(cx); this.change_selections_inner(Some(Autoscroll::fit()), false, cx, |s| { s.select(new_selections) @@ -4043,7 +4059,7 @@ impl Editor { } } - (selection.clone(), enclosing) + (selection, enclosing) }) } @@ -4741,29 +4757,7 @@ impl Editor { .as_ref() .zip(editor.project.clone()) .map(|(tasks, project)| { - let position = Point::new(buffer_row, tasks.column); - let range_start = buffer.read(cx).anchor_at(position, Bias::Right); - let location = Location { - buffer: buffer.clone(), - range: range_start..range_start, - }; - // Fill in the environmental variables from the tree-sitter captures - let mut captured_task_variables = TaskVariables::default(); - for (capture_name, value) in tasks.extra_variables.clone() { - captured_task_variables.insert( - task::VariableName::Custom(capture_name.into()), - value.clone(), - ); - } - project.update(cx, |project, cx| { - project.task_store().update(cx, |task_store, cx| { - task_store.task_context_for_location( - captured_task_variables, - location, - cx, - ) - }) - }) + Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx) }); Some(cx.spawn(|editor, mut cx| async move { @@ -4774,15 +4768,7 @@ impl Editor { let resolved_tasks = tasks.zip(task_context).map(|(tasks, task_context)| { Arc::new(ResolvedTasks { - templates: tasks - .templates - .iter() - .filter_map(|(kind, template)| { - template - .resolve_task(&kind.to_id_base(), &task_context) - .map(|task| (kind.clone(), task)) - }) - .collect(), + templates: tasks.resolve(&task_context).collect(), position: snapshot.buffer_snapshot.anchor_before(Point::new( multibuffer_point.row, tasks.column, @@ -5714,6 +5700,132 @@ impl Editor { })) } + fn build_tasks_context( + project: &Model, + buffer: &Model, + buffer_row: u32, + tasks: &Arc, + cx: &mut ViewContext, + ) -> Task> { + let position = Point::new(buffer_row, tasks.column); + let range_start = buffer.read(cx).anchor_at(position, Bias::Right); + let location = Location { + buffer: buffer.clone(), + range: range_start..range_start, + }; + // Fill in the environmental variables from the tree-sitter captures + let mut captured_task_variables = TaskVariables::default(); + for (capture_name, value) in tasks.extra_variables.clone() { + captured_task_variables.insert( + task::VariableName::Custom(capture_name.into()), + value.clone(), + ); + } + project.update(cx, |project, cx| { + project.task_store().update(cx, |task_store, cx| { + task_store.task_context_for_location(captured_task_variables, location, cx) + }) + }) + } + + pub fn spawn_nearest_task(&mut self, action: &SpawnNearestTask, cx: &mut ViewContext) { + let Some((workspace, _)) = self.workspace.clone() else { + return; + }; + let Some(project) = self.project.clone() else { + return; + }; + + // Try to find a closest, enclosing node using tree-sitter that has a + // task + let Some((buffer, buffer_row, tasks)) = self + .find_enclosing_node_task(cx) + // Or find the task that's closest in row-distance. + .or_else(|| self.find_closest_task(cx)) + else { + return; + }; + + let reveal_strategy = action.reveal; + let task_context = Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx); + cx.spawn(|_, mut cx| async move { + let context = task_context.await?; + let (task_source_kind, mut resolved_task) = tasks.resolve(&context).next()?; + + let resolved = resolved_task.resolved.as_mut()?; + resolved.reveal = reveal_strategy; + + workspace + .update(&mut cx, |workspace, cx| { + workspace::tasks::schedule_resolved_task( + workspace, + task_source_kind, + resolved_task, + false, + cx, + ); + }) + .ok() + }) + .detach(); + } + + fn find_closest_task( + &mut self, + cx: &mut ViewContext, + ) -> Option<(Model, u32, Arc)> { + let cursor_row = self.selections.newest_adjusted(cx).head().row; + + let ((buffer_id, row), tasks) = self + .tasks + .iter() + .min_by_key(|((_, row), _)| cursor_row.abs_diff(*row))?; + + let buffer = self.buffer.read(cx).buffer(*buffer_id)?; + let tasks = Arc::new(tasks.to_owned()); + Some((buffer, *row, tasks)) + } + + fn find_enclosing_node_task( + &mut self, + cx: &mut ViewContext, + ) -> Option<(Model, u32, Arc)> { + let snapshot = self.buffer.read(cx).snapshot(cx); + let offset = self.selections.newest::(cx).head(); + let excerpt = snapshot.excerpt_containing(offset..offset)?; + let buffer_id = excerpt.buffer().remote_id(); + + let layer = excerpt.buffer().syntax_layer_at(offset)?; + let mut cursor = layer.node().walk(); + + while cursor.goto_first_child_for_byte(offset).is_some() { + if cursor.node().end_byte() == offset { + cursor.goto_next_sibling(); + } + } + + // Ascend to the smallest ancestor that contains the range and has a task. + loop { + let node = cursor.node(); + let node_range = node.byte_range(); + let symbol_start_row = excerpt.buffer().offset_to_point(node.start_byte()).row; + + // Check if this node contains our offset + if node_range.start <= offset && node_range.end >= offset { + // If it contains offset, check for task + if let Some(tasks) = self.tasks.get(&(buffer_id, symbol_start_row)) { + let buffer = self.buffer.read(cx).buffer(buffer_id)?; + return Some((buffer, symbol_start_row, Arc::new(tasks.to_owned()))); + } + } + + if !cursor.goto_parent() { + break; + } + } + None + } + fn render_run_indicator( &self, _style: &EditorStyle, @@ -9011,14 +9123,22 @@ impl Editor { let snapshot = this.buffer.read(cx).read(cx); let empty_str: Arc = Arc::default(); let mut suffixes_inserted = Vec::new(); + let ignore_indent = action.ignore_indent; fn comment_prefix_range( snapshot: &MultiBufferSnapshot, row: MultiBufferRow, comment_prefix: &str, comment_prefix_whitespace: &str, + ignore_indent: bool, ) -> Range { - let start = Point::new(row.0, snapshot.indent_size_for_line(row).len); + let indent_size = if ignore_indent { + 0 + } else { + snapshot.indent_size_for_line(row).len + }; + + let start = Point::new(row.0, indent_size); let mut line_bytes = snapshot .bytes_in_range(start..snapshot.max_point()) @@ -9114,7 +9234,16 @@ impl Editor { } // If the language has line comments, toggle those. - let full_comment_prefixes = language.line_comment_prefixes(); + let mut full_comment_prefixes = language.line_comment_prefixes().to_vec(); + + // If ignore_indent is set, trim spaces from the right side of all full_comment_prefixes + if ignore_indent { + full_comment_prefixes = full_comment_prefixes + .into_iter() + .map(|s| Arc::from(s.trim_end())) + .collect(); + } + if !full_comment_prefixes.is_empty() { let first_prefix = full_comment_prefixes .first() @@ -9141,6 +9270,7 @@ impl Editor { row, &prefix[..trimmed_prefix_len], &prefix[trimmed_prefix_len..], + ignore_indent, ) }) .max_by_key(|range| range.end.column - range.start.column) @@ -9181,6 +9311,7 @@ impl Editor { start_row, comment_prefix, comment_prefix_whitespace, + ignore_indent, ); let suffix_range = comment_suffix_range( snapshot.deref(), diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index d56b22b454..43e7ff74e6 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -3989,6 +3989,76 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + cx.set_state( + &" + ˇzero + one + two + three + four + five + " + .unindent(), + ); + + // Create a four-line block that replaces three lines of text. + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let snapshot = &snapshot.buffer_snapshot; + let placement = BlockPlacement::Replace( + snapshot.anchor_after(Point::new(1, 0))..snapshot.anchor_after(Point::new(3, 0)), + ); + editor.insert_blocks( + [BlockProperties { + placement, + height: 4, + style: BlockStyle::Sticky, + render: Box::new(|_| gpui::div().into_any_element()), + priority: 0, + }], + None, + cx, + ); + }); + + // Move down so that the cursor touches the block. + cx.update_editor(|editor, cx| { + editor.move_down(&Default::default(), cx); + }); + cx.assert_editor_state( + &" + zero + «one + two + threeˇ» + four + five + " + .unindent(), + ); + + // Move down past the block. + cx.update_editor(|editor, cx| { + editor.move_down(&Default::default(), cx); + }); + cx.assert_editor_state( + &" + zero + one + two + three + ˇfour + five + " + .unindent(), + ); +} + #[gpui::test] fn test_transpose(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -4182,7 +4252,7 @@ async fn test_rewrap(cx: &mut TestAppContext) { // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis - // porttitor id. Aliquam id accumsan eros.ˇˇˇˇ + // porttitor id. Aliquam id accumsan eros.ˇ "}; cx.set_state(unwrapped_text); @@ -4212,7 +4282,7 @@ async fn test_rewrap(cx: &mut TestAppContext) { let wrapped_text = indoc! {" // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus - // auctor, eu lacinia sapien scelerisque.ˇˇ + // auctor, eu lacinia sapien scelerisque.ˇ // // Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, @@ -4220,7 +4290,7 @@ async fn test_rewrap(cx: &mut TestAppContext) { // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id - // vulputate turpis porttitor id. Aliquam id accumsan eros.ˇˇ + // vulputate turpis porttitor id. Aliquam id accumsan eros.ˇ "}; cx.set_state(unwrapped_text); @@ -8533,6 +8603,131 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { "}); } +#[gpui::test] +async fn test_toggle_comment_ignore_indent(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + let language = Arc::new(Language::new( + LanguageConfig { + line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()], + ..Default::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + )); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + let toggle_comments = &ToggleComments { + advance_downwards: false, + ignore_indent: true, + }; + + // If multiple selections intersect a line, the line is only toggled once. + cx.set_state(indoc! {" + fn a() { + // «b(); + // c(); + // ˇ» d(); + } + "}); + + cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + «b(); + c(); + ˇ» d(); + } + "}); + + // The comment prefix is inserted at the beginning of each line + cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + // «b(); + // c(); + // ˇ» d(); + } + "}); + + // If a selection ends at the beginning of a line, that line is not toggled. + cx.set_selections_state(indoc! {" + fn a() { + // b(); + // «c(); + ˇ»// d(); + } + "}); + + cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + // b(); + «c(); + ˇ»// d(); + } + "}); + + // If a selection span a single line and is empty, the line is toggled. + cx.set_state(indoc! {" + fn a() { + a(); + b(); + ˇ + } + "}); + + cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + a(); + b(); + //ˇ + } + "}); + + // If a selection span multiple lines, empty lines are not toggled. + cx.set_state(indoc! {" + fn a() { + «a(); + + c();ˇ» + } + "}); + + cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + // «a(); + + // c();ˇ» + } + "}); + + // If a selection includes multiple comment prefixes, all lines are uncommented. + cx.set_state(indoc! {" + fn a() { + // «a(); + /// b(); + //! c();ˇ» + } + "}); + + cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + «a(); + b(); + c();ˇ» + } + "}); +} + #[gpui::test] async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); @@ -8554,6 +8749,7 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) let toggle_comments = &ToggleComments { advance_downwards: true, + ignore_indent: false, }; // Single cursor on one line -> advance @@ -13204,6 +13400,89 @@ async fn test_goto_definition_with_find_all_references_fallback(cx: &mut gpui::T }); } +#[gpui::test] +async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new(Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::LANGUAGE.into()), + )); + + let text = r#" + #[cfg(test)] + mod tests() { + #[test] + fn runnable_1() { + let a = 1; + } + + #[test] + fn runnable_2() { + let a = 1; + let b = 2; + } + } + "# + .unindent(); + + let fs = FakeFs::new(cx.executor()); + fs.insert_file("/file.rs", Default::default()).await; + + let project = Project::test(fs, ["/a".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx)); + let multi_buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); + + let editor = cx.new_view(|cx| { + Editor::new( + EditorMode::Full, + multi_buffer, + Some(project.clone()), + true, + cx, + ) + }); + + editor.update(cx, |editor, cx| { + editor.tasks.insert( + (buffer.read(cx).remote_id(), 3), + RunnableTasks { + templates: vec![], + offset: MultiBufferOffset(43), + column: 0, + extra_variables: HashMap::default(), + context_range: BufferOffset(43)..BufferOffset(85), + }, + ); + editor.tasks.insert( + (buffer.read(cx).remote_id(), 8), + RunnableTasks { + templates: vec![], + offset: MultiBufferOffset(86), + column: 0, + extra_variables: HashMap::default(), + context_range: BufferOffset(86)..BufferOffset(191), + }, + ); + + // Test finding task when cursor is inside function body + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(4, 5)..Point::new(4, 5)]) + }); + let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap(); + assert_eq!(row, 3, "Should find task for cursor inside runnable_1"); + + // Test finding task when cursor is on function name + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(8, 4)..Point::new(8, 4)]) + }); + let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap(); + assert_eq!(row, 8, "Should find task when cursor is on function name"); + }); +} + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(DisplayRow(row as u32), column as u32); point..point diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2d87cd4a3a..66596fdf8b 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -25,7 +25,7 @@ use crate::{ MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, }; use client::ParticipantIndex; -use collections::{BTreeMap, HashMap}; +use collections::{BTreeMap, HashMap, HashSet}; use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid}; use gpui::Subscription; use gpui::{ @@ -451,7 +451,8 @@ impl EditorElement { register_action(view, cx, Editor::apply_selected_diff_hunks); register_action(view, cx, Editor::open_active_item_in_terminal); register_action(view, cx, Editor::toggle_breakpoint); - register_action(view, cx, Editor::reload_file) + register_action(view, cx, Editor::reload_file); + register_action(view, cx, Editor::spawn_nearest_task); } fn register_key_listeners(&self, cx: &mut WindowContext, layout: &EditorLayout) { @@ -821,10 +822,12 @@ impl EditorElement { cx.notify() } + #[allow(clippy::too_many_arguments)] fn layout_selections( &self, start_anchor: Anchor, end_anchor: Anchor, + local_selections: &[Selection], snapshot: &EditorSnapshot, start_row: DisplayRow, end_row: DisplayRow, @@ -839,13 +842,9 @@ impl EditorElement { let mut newest_selection_head = None; self.editor.update(cx, |editor, cx| { if editor.show_local_selections { - let mut local_selections: Vec> = editor - .selections - .disjoint_in_range(start_anchor..end_anchor, cx); - local_selections.extend(editor.selections.pending(cx)); let mut layouts = Vec::new(); let newest = editor.selections.newest(cx); - for selection in local_selections.drain(..) { + for selection in local_selections.iter().cloned() { let is_empty = selection.start == selection.end; let is_newest = selection == newest; @@ -1008,6 +1007,7 @@ impl EditorElement { &self, snapshot: &EditorSnapshot, selections: &[(PlayerColor, Vec)], + block_start_rows: &HashSet, visible_display_row_range: Range, line_layouts: &[LineWithInvisibles], text_hitbox: &Hitbox, @@ -1027,7 +1027,10 @@ impl EditorElement { let cursor_position = selection.head; let in_range = visible_display_row_range.contains(&cursor_position.row()); - if (selection.is_local && !editor.show_local_cursors(cx)) || !in_range { + if (selection.is_local && !editor.show_local_cursors(cx)) + || !in_range + || block_start_rows.contains(&cursor_position.row()) + { continue; } @@ -2154,14 +2157,14 @@ impl EditorElement { editor_width: Pixels, scroll_width: &mut Pixels, resized_blocks: &mut HashMap, + selections: &[Selection], cx: &mut WindowContext, ) -> (AnyElement, Size) { let mut element = match block { Block::Custom(block) => { - let align_to = block - .start() - .to_point(&snapshot.buffer_snapshot) - .to_display_point(snapshot); + let block_start = block.start().to_point(&snapshot.buffer_snapshot); + let block_end = block.end().to_point(&snapshot.buffer_snapshot); + let align_to = block_start.to_display_point(snapshot); let anchor_x = text_x + if rows.contains(&align_to.row()) { line_layouts[align_to.row().minus(rows.start) as usize] @@ -2171,6 +2174,18 @@ impl EditorElement { .x_for_index(align_to.column() as usize) }; + let selected = selections + .binary_search_by(|selection| { + if selection.end <= block_start { + Ordering::Less + } else if selection.start >= block_end { + Ordering::Greater + } else { + Ordering::Equal + } + }) + .is_ok(); + div() .size_full() .child(block.render(&mut BlockContext { @@ -2180,6 +2195,7 @@ impl EditorElement { line_height, em_width, block_id, + selected, max_width: text_hitbox.size.width.max(*scroll_width), editor_style: &self.style, })) @@ -2517,6 +2533,7 @@ impl EditorElement { text_x: Pixels, line_height: Pixels, line_layouts: &[LineWithInvisibles], + selections: &[Selection], cx: &mut WindowContext, ) -> Result, HashMap> { let (fixed_blocks, non_fixed_blocks) = snapshot @@ -2553,6 +2570,7 @@ impl EditorElement { editor_width, scroll_width, &mut resized_blocks, + selections, cx, ); fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width); @@ -2597,6 +2615,7 @@ impl EditorElement { editor_width, scroll_width, &mut resized_blocks, + selections, cx, ); @@ -2642,6 +2661,7 @@ impl EditorElement { editor_width, scroll_width, &mut resized_blocks, + selections, cx, ); @@ -2670,6 +2690,7 @@ impl EditorElement { fn layout_blocks( &self, blocks: &mut Vec, + block_starts: &mut HashSet, hitbox: &Hitbox, line_height: Pixels, scroll_pixel_position: gpui::Point, @@ -2677,6 +2698,7 @@ impl EditorElement { ) { for block in blocks { let mut origin = if let Some(row) = block.row { + block_starts.insert(row); hitbox.origin + point( Pixels::ZERO, @@ -5195,9 +5217,19 @@ impl Element for EditorElement { cx, ); + let local_selections: Vec> = + self.editor.update(cx, |editor, cx| { + let mut selections = editor + .selections + .disjoint_in_range(start_anchor..end_anchor, cx); + selections.extend(editor.selections.pending(cx)); + selections + }); + let (selections, active_rows, newest_selection_head) = self.layout_selections( start_anchor, end_anchor, + &local_selections, &snapshot, start_row, end_row, @@ -5290,6 +5322,7 @@ impl Element for EditorElement { gutter_dimensions.full_width(), line_height, &line_layouts, + &local_selections, cx, ) }); @@ -5429,9 +5462,11 @@ impl Element for EditorElement { cx, ); + let mut block_start_rows = HashSet::default(); cx.with_element_namespace("blocks", |cx| { self.layout_blocks( &mut blocks, + &mut block_start_rows, &hitbox, line_height, scroll_pixel_position, @@ -5448,6 +5483,7 @@ impl Element for EditorElement { let visible_cursors = self.layout_visible_cursors( &snapshot, &selections, + &block_start_rows, start_row..end_row, &line_layouts, &text_hitbox, diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index fb198c837c..f079939787 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1351,6 +1351,61 @@ mod tests { }); } + #[gpui::test] + // https://github.com/zed-industries/zed/issues/15498 + async fn test_info_hover_with_hrs(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + fn fuˇnc(abc def: i32) -> u32 { + } + "}); + + cx.lsp.handle_request::({ + |_, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: indoc!( + r#" + ### function `errands_data_read` + + --- + → `char *` + Function to read a file into a string + + --- + ```cpp + static char *errands_data_read() + ``` + "# + ) + .to_string(), + }), + range: None, + })) + } + }); + cx.update_editor(|editor, cx| hover(editor, &Default::default(), cx)); + cx.run_until_parked(); + + cx.update_editor(|editor, cx| { + let popover = editor.hover_state.info_popovers.first().unwrap(); + let content = popover.get_rendered_text(cx); + + assert!(content.contains("Function to read a file")); + }); + } + #[gpui::test] async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 8e1c12b8cd..1edfc6a4fb 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -1,6 +1,6 @@ use std::{ cell::Ref, - iter, mem, + cmp, iter, mem, ops::{Deref, DerefMut, Range, Sub}, sync::Arc, }; @@ -98,9 +98,9 @@ impl SelectionsCollection { &self, cx: &mut AppContext, ) -> Option> { - self.pending_anchor() - .as_ref() - .map(|pending| pending.map(|p| p.summary::(&self.buffer(cx)))) + let map = self.display_map(cx); + let selection = resolve_selections(self.pending_anchor().as_ref(), &map).next(); + selection } pub(crate) fn pending_mode(&self) -> Option { @@ -111,12 +111,10 @@ impl SelectionsCollection { where D: 'a + TextDimension + Ord + Sub, { + let map = self.display_map(cx); let disjoint_anchors = &self.disjoint; - let mut disjoint = - resolve_multiple::(disjoint_anchors.iter(), &self.buffer(cx)).peekable(); - + let mut disjoint = resolve_selections::(disjoint_anchors.iter(), &map).peekable(); let mut pending_opt = self.pending::(cx); - iter::from_fn(move || { if let Some(pending) = pending_opt.as_mut() { while let Some(next_selection) = disjoint.peek() { @@ -199,34 +197,57 @@ impl SelectionsCollection { where D: 'a + TextDimension + Ord + Sub + std::fmt::Debug, { - let buffer = self.buffer(cx); + let map = self.display_map(cx); let start_ix = match self .disjoint - .binary_search_by(|probe| probe.end.cmp(&range.start, &buffer)) + .binary_search_by(|probe| probe.end.cmp(&range.start, &map.buffer_snapshot)) { Ok(ix) | Err(ix) => ix, }; let end_ix = match self .disjoint - .binary_search_by(|probe| probe.start.cmp(&range.end, &buffer)) + .binary_search_by(|probe| probe.start.cmp(&range.end, &map.buffer_snapshot)) { Ok(ix) => ix + 1, Err(ix) => ix, }; - resolve_multiple(&self.disjoint[start_ix..end_ix], &buffer).collect() + resolve_selections(&self.disjoint[start_ix..end_ix], &map).collect() } pub fn all_display( &self, cx: &mut AppContext, ) -> (DisplaySnapshot, Vec>) { - let display_map = self.display_map(cx); - let selections = self - .all::(cx) - .into_iter() - .map(|selection| selection.map(|point| point.to_display_point(&display_map))) - .collect(); - (display_map, selections) + let map = self.display_map(cx); + let disjoint_anchors = &self.disjoint; + let mut disjoint = resolve_selections_display(disjoint_anchors.iter(), &map).peekable(); + let mut pending_opt = + resolve_selections_display(self.pending_anchor().as_ref(), &map).next(); + let selections = iter::from_fn(move || { + if let Some(pending) = pending_opt.as_mut() { + while let Some(next_selection) = disjoint.peek() { + if pending.start <= next_selection.end && pending.end >= next_selection.start { + let next_selection = disjoint.next().unwrap(); + if next_selection.start < pending.start { + pending.start = next_selection.start; + } + if next_selection.end > pending.end { + pending.end = next_selection.end; + } + } else if next_selection.end < pending.start { + return disjoint.next(); + } else { + break; + } + } + + pending_opt.take() + } else { + disjoint.next() + } + }) + .collect(); + (map, selections) } pub fn newest_anchor(&self) -> &Selection { @@ -241,15 +262,18 @@ impl SelectionsCollection { &self, cx: &mut AppContext, ) -> Selection { - let buffer = self.buffer(cx); - self.newest_anchor().map(|p| p.summary::(&buffer)) + let map = self.display_map(cx); + let selection = resolve_selections([self.newest_anchor()], &map) + .next() + .unwrap(); + selection } pub fn newest_display(&self, cx: &mut AppContext) -> Selection { - let display_map = self.display_map(cx); - let selection = self - .newest_anchor() - .map(|point| point.to_display_point(&display_map)); + let map = self.display_map(cx); + let selection = resolve_selections_display([self.newest_anchor()], &map) + .next() + .unwrap(); selection } @@ -265,8 +289,11 @@ impl SelectionsCollection { &self, cx: &mut AppContext, ) -> Selection { - let buffer = self.buffer(cx); - self.oldest_anchor().map(|p| p.summary::(&buffer)) + let map = self.display_map(cx); + let selection = resolve_selections([self.oldest_anchor()], &map) + .next() + .unwrap(); + selection } pub fn first_anchor(&self) -> Selection { @@ -538,9 +565,9 @@ impl<'a> MutableSelectionsCollection<'a> { } pub fn select_anchors(&mut self, selections: Vec>) { - let buffer = self.buffer.read(self.cx).snapshot(self.cx); + let map = self.display_map(); let resolved_selections = - resolve_multiple::(&selections, &buffer).collect::>(); + resolve_selections::(&selections, &map).collect::>(); self.select(resolved_selections); } @@ -650,20 +677,16 @@ impl<'a> MutableSelectionsCollection<'a> { ) { let mut changed = false; let display_map = self.display_map(); - let selections = self - .collection - .all::(self.cx) + let (_, selections) = self.collection.all_display(self.cx); + let selections = selections .into_iter() .map(|selection| { - let mut moved_selection = - selection.map(|point| point.to_display_point(&display_map)); + let mut moved_selection = selection.clone(); move_selection(&display_map, &mut moved_selection); - let moved_selection = - moved_selection.map(|display_point| display_point.to_point(&display_map)); if selection != moved_selection { changed = true; } - moved_selection + moved_selection.map(|display_point| display_point.to_point(&display_map)) }) .collect(); @@ -804,8 +827,8 @@ impl<'a> MutableSelectionsCollection<'a> { .collect(); if !adjusted_disjoint.is_empty() { - let resolved_selections = - resolve_multiple(adjusted_disjoint.iter(), &self.buffer()).collect(); + let map = self.display_map(); + let resolved_selections = resolve_selections(adjusted_disjoint.iter(), &map).collect(); self.select::(resolved_selections); } @@ -849,27 +872,76 @@ impl<'a> DerefMut for MutableSelectionsCollection<'a> { } // Panics if passed selections are not in order -pub(crate) fn resolve_multiple<'a, D, I>( - selections: I, - snapshot: &MultiBufferSnapshot, -) -> impl 'a + Iterator> -where - D: TextDimension + Ord + Sub, - I: 'a + IntoIterator>, -{ +fn resolve_selections_display<'a>( + selections: impl 'a + IntoIterator>, + map: &'a DisplaySnapshot, +) -> impl 'a + Iterator> { let (to_summarize, selections) = selections.into_iter().tee(); - let mut summaries = snapshot - .summaries_for_anchors::( - to_summarize - .flat_map(|s| [&s.start, &s.end]) - .collect::>(), - ) + let mut summaries = map + .buffer_snapshot + .summaries_for_anchors::(to_summarize.flat_map(|s| [&s.start, &s.end])) .into_iter(); - selections.map(move |s| Selection { - id: s.id, - start: summaries.next().unwrap(), - end: summaries.next().unwrap(), - reversed: s.reversed, - goal: s.goal, + let mut selections = selections + .map(move |s| { + let start = summaries.next().unwrap(); + let end = summaries.next().unwrap(); + + let display_start = map.point_to_display_point(start, Bias::Left); + let display_end = if start == end { + map.point_to_display_point(end, Bias::Right) + } else { + map.point_to_display_point(end, Bias::Left) + }; + + Selection { + id: s.id, + start: display_start, + end: display_end, + reversed: s.reversed, + goal: s.goal, + } + }) + .peekable(); + iter::from_fn(move || { + let mut selection = selections.next()?; + while let Some(next_selection) = selections.peek() { + if selection.end >= next_selection.start { + selection.end = cmp::max(selection.end, next_selection.end); + selections.next(); + } else { + break; + } + } + Some(selection) + }) +} + +// Panics if passed selections are not in order +pub(crate) fn resolve_selections<'a, D, I>( + selections: I, + map: &'a DisplaySnapshot, +) -> impl 'a + Iterator> +where + D: TextDimension + Clone + Ord + Sub, + I: 'a + IntoIterator>, +{ + let (to_convert, selections) = resolve_selections_display(selections, map).tee(); + let mut converted_endpoints = + map.buffer_snapshot + .dimensions_from_points::(to_convert.flat_map(|s| { + let start = map.display_point_to_point(s.start, Bias::Left); + let end = map.display_point_to_point(s.end, Bias::Right); + [start, end] + })); + selections.map(move |s| { + let start = converted_endpoints.next().unwrap(); + let end = converted_endpoints.next().unwrap(); + Selection { + id: s.id, + start, + end, + reversed: s.reversed, + goal: s.goal, + } }) } diff --git a/crates/extension/Cargo.toml b/crates/extension/Cargo.toml index 26b8610e76..824e00b468 100644 --- a/crates/extension/Cargo.toml +++ b/crates/extension/Cargo.toml @@ -9,59 +9,23 @@ license = "GPL-3.0-or-later" workspace = true [lib] -path = "src/extension_store.rs" -doctest = false - -[features] -no-webrtc = ["workspace/no-webrtc"] +path = "src/extension.rs" [dependencies] anyhow.workspace = true -assistant_slash_command.workspace = true async-compression.workspace = true async-tar.workspace = true -async-trait.workspace = true -client.workspace = true collections.workspace = true fs.workspace = true futures.workspace = true -gpui.workspace = true http_client.workspace = true -indexed_docs.workspace = true language.workspace = true log.workspace = true lsp.workspace = true -node_runtime.workspace = true -paths.workspace = true -project.workspace = true -release_channel.workspace = true -schemars.workspace = true semantic_version.workspace = true serde.workspace = true serde_json.workspace = true -serde_json_lenient.workspace = true -settings.workspace = true -snippet_provider.workspace = true -task.workspace = true -theme.workspace = true toml.workspace = true -ui.workspace = true -url.workspace = true -util.workspace = true wasm-encoder.workspace = true wasmparser.workspace = true -wasmtime-wasi.workspace = true -wasmtime.workspace = true wit-component.workspace = true -workspace.workspace = true - -[dev-dependencies] -ctor.workspace = true -env_logger.workspace = true -fs = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -parking_lot.workspace = true -project = { workspace = true, features = ["test-support"] } -reqwest_client.workspace = true -workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/extension/src/extension.rs b/crates/extension/src/extension.rs new file mode 100644 index 0000000000..3635096d54 --- /dev/null +++ b/crates/extension/src/extension.rs @@ -0,0 +1,50 @@ +pub mod extension_builder; +mod extension_manifest; + +use anyhow::{anyhow, bail, Context as _, Result}; +use semantic_version::SemanticVersion; + +pub use crate::extension_manifest::*; + +pub fn parse_wasm_extension_version( + extension_id: &str, + wasm_bytes: &[u8], +) -> Result { + let mut version = None; + + for part in wasmparser::Parser::new(0).parse_all(wasm_bytes) { + if let wasmparser::Payload::CustomSection(s) = + part.context("error parsing wasm extension")? + { + if s.name() == "zed:api-version" { + version = parse_wasm_extension_version_custom_section(s.data()); + if version.is_none() { + bail!( + "extension {} has invalid zed:api-version section: {:?}", + extension_id, + s.data() + ); + } + } + } + } + + // The reason we wait until we're done parsing all of the Wasm bytes to return the version + // is to work around a panic that can happen inside of Wasmtime when the bytes are invalid. + // + // By parsing the entirety of the Wasm bytes before we return, we're able to detect this problem + // earlier as an `Err` rather than as a panic. + version.ok_or_else(|| anyhow!("extension {} has no zed:api-version section", extension_id)) +} + +fn parse_wasm_extension_version_custom_section(data: &[u8]) -> Option { + if data.len() == 6 { + Some(SemanticVersion::new( + u16::from_be_bytes([data[0], data[1]]) as _, + u16::from_be_bytes([data[2], data[3]]) as _, + u16::from_be_bytes([data[4], data[5]]) as _, + )) + } else { + None + } +} diff --git a/crates/extension/src/extension_builder.rs b/crates/extension/src/extension_builder.rs index 7380e699f9..a92d878d1e 100644 --- a/crates/extension/src/extension_builder.rs +++ b/crates/extension/src/extension_builder.rs @@ -1,6 +1,6 @@ -use crate::wasm_host::parse_wasm_extension_version; -use crate::ExtensionManifest; -use crate::{extension_manifest::ExtensionLibraryKind, GrammarManifestEntry}; +use crate::{ + parse_wasm_extension_version, ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry, +}; use anyhow::{anyhow, bail, Context as _, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; diff --git a/crates/extension_cli/Cargo.toml b/crates/extension_cli/Cargo.toml index 6c7e3bdc62..d7ec5aa7cf 100644 --- a/crates/extension_cli/Cargo.toml +++ b/crates/extension_cli/Cargo.toml @@ -16,7 +16,7 @@ path = "src/main.rs" anyhow.workspace = true clap = { workspace = true, features = ["derive"] } env_logger.workspace = true -extension = { workspace = true, features = ["no-webrtc"] } +extension.workspace = true fs.workspace = true language.workspace = true log.workspace = true diff --git a/crates/extension_cli/src/main.rs b/crates/extension_cli/src/main.rs index dd6f221378..ffa9555c21 100644 --- a/crates/extension_cli/src/main.rs +++ b/crates/extension_cli/src/main.rs @@ -15,7 +15,6 @@ use extension::{ }; use language::LanguageConfig; use reqwest_client::ReqwestClient; -use theme::ThemeRegistry; use tree_sitter::{Language, Query, WasmStore}; #[derive(Parser, Debug)] @@ -267,7 +266,7 @@ async fn test_themes( ) -> Result<()> { for relative_theme_path in &manifest.themes { let theme_path = extension_path.join(relative_theme_path); - let theme_family = ThemeRegistry::read_user_theme(&theme_path, fs.clone()).await?; + let theme_family = theme::read_user_theme(&theme_path, fs.clone()).await?; log::info!("loaded theme family {}", theme_family.name); } diff --git a/crates/extension_host/Cargo.toml b/crates/extension_host/Cargo.toml new file mode 100644 index 0000000000..4eb5d22f72 --- /dev/null +++ b/crates/extension_host/Cargo.toml @@ -0,0 +1,62 @@ +[package] +name = "extension_host" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/extension_host.rs" +doctest = false + +[dependencies] +anyhow.workspace = true +assistant_slash_command.workspace = true +async-compression.workspace = true +async-tar.workspace = true +async-trait.workspace = true +client.workspace = true +collections.workspace = true +extension.workspace = true +fs.workspace = true +futures.workspace = true +gpui.workspace = true +http_client.workspace = true +indexed_docs.workspace = true +language.workspace = true +log.workspace = true +lsp.workspace = true +node_runtime.workspace = true +paths.workspace = true +project.workspace = true +release_channel.workspace = true +schemars.workspace = true +semantic_version.workspace = true +serde.workspace = true +serde_json.workspace = true +serde_json_lenient.workspace = true +settings.workspace = true +snippet_provider.workspace = true +task.workspace = true +theme.workspace = true +toml.workspace = true +ui.workspace = true +url.workspace = true +util.workspace = true +wasmtime-wasi.workspace = true +wasmtime.workspace = true +workspace.workspace = true + +[dev-dependencies] +ctor.workspace = true +env_logger.workspace = true +fs = { workspace = true, features = ["test-support"] } +gpui = { workspace = true, features = ["test-support"] } +language = { workspace = true, features = ["test-support"] } +parking_lot.workspace = true +project = { workspace = true, features = ["test-support"] } +reqwest_client.workspace = true +workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/extension_host/LICENSE-GPL b/crates/extension_host/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/extension_host/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/extension/build.rs b/crates/extension_host/build.rs similarity index 100% rename from crates/extension/build.rs rename to crates/extension_host/build.rs diff --git a/crates/extension/src/extension_store.rs b/crates/extension_host/src/extension_host.rs similarity index 99% rename from crates/extension/src/extension_store.rs rename to crates/extension_host/src/extension_host.rs index 0a9299a8be..b27a486b4b 100644 --- a/crates/extension/src/extension_store.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1,7 +1,5 @@ -pub mod extension_builder; mod extension_indexed_docs_provider; mod extension_lsp_adapter; -mod extension_manifest; mod extension_settings; mod extension_slash_command; mod wasm_host; @@ -10,7 +8,6 @@ mod wasm_host; mod extension_store_test; use crate::extension_indexed_docs_provider::ExtensionIndexedDocsProvider; -use crate::extension_manifest::SchemaVersion; use crate::extension_slash_command::ExtensionSlashCommand; use crate::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host::wit}; use anyhow::{anyhow, bail, Context as _, Result}; @@ -19,7 +16,8 @@ use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use client::{telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse}; use collections::{btree_map, BTreeMap, HashSet}; -use extension_builder::{CompileExtensionOptions, ExtensionBuilder}; +use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; +use extension::SchemaVersion; use fs::{Fs, RemoveOptions}; use futures::{ channel::{ @@ -62,7 +60,7 @@ use wasm_host::{ WasmExtension, WasmHost, }; -pub use extension_manifest::{ +pub use extension::{ ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry, OldExtensionManifest, }; pub use extension_settings::ExtensionSettings; @@ -1358,7 +1356,7 @@ impl ExtensionStore { continue; }; - let Some(theme_family) = ThemeRegistry::read_user_theme(&theme_path, fs.clone()) + let Some(theme_family) = theme::read_user_theme(&theme_path, fs.clone()) .await .log_err() else { diff --git a/crates/extension/src/extension_indexed_docs_provider.rs b/crates/extension_host/src/extension_indexed_docs_provider.rs similarity index 100% rename from crates/extension/src/extension_indexed_docs_provider.rs rename to crates/extension_host/src/extension_indexed_docs_provider.rs diff --git a/crates/extension/src/extension_lsp_adapter.rs b/crates/extension_host/src/extension_lsp_adapter.rs similarity index 100% rename from crates/extension/src/extension_lsp_adapter.rs rename to crates/extension_host/src/extension_lsp_adapter.rs diff --git a/crates/extension/src/extension_settings.rs b/crates/extension_host/src/extension_settings.rs similarity index 100% rename from crates/extension/src/extension_settings.rs rename to crates/extension_host/src/extension_settings.rs diff --git a/crates/extension/src/extension_slash_command.rs b/crates/extension_host/src/extension_slash_command.rs similarity index 100% rename from crates/extension/src/extension_slash_command.rs rename to crates/extension_host/src/extension_slash_command.rs diff --git a/crates/extension/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs similarity index 99% rename from crates/extension/src/extension_store_test.rs rename to crates/extension_host/src/extension_store_test.rs index 1274fafc3c..bd571703db 100644 --- a/crates/extension/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -1,4 +1,3 @@ -use crate::extension_manifest::SchemaVersion; use crate::extension_settings::ExtensionSettings; use crate::{ Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry, @@ -8,6 +7,7 @@ use crate::{ use assistant_slash_command::SlashCommandRegistry; use async_compression::futures::bufread::GzipEncoder; use collections::BTreeMap; +use extension::SchemaVersion; use fs::{FakeFs, Fs, RealFs}; use futures::{io::BufReader, AsyncReadExt, StreamExt}; use gpui::{Context, SemanticVersion, TestAppContext}; @@ -296,7 +296,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { ["ERB", "Plain Text", "Ruby"] ); assert_eq!( - theme_registry.list_names(false), + theme_registry.list_names(), [ "Monokai Dark", "Monokai Light", @@ -377,7 +377,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { assert_eq!(index.themes, expected_index.themes); assert_eq!( - theme_registry.list_names(false), + theme_registry.list_names(), [ "Gruvbox", "Monokai Dark", @@ -424,7 +424,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { ["embedded_template".into(), "ruby".into()] ); assert_eq!( - theme_registry.list_names(false), + theme_registry.list_names(), [ "Gruvbox", "Monokai Dark", diff --git a/crates/extension/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs similarity index 82% rename from crates/extension/src/wasm_host.rs rename to crates/extension_host/src/wasm_host.rs index b3fd13a5ba..4241f3f551 100644 --- a/crates/extension/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -1,7 +1,7 @@ pub(crate) mod wit; use crate::ExtensionManifest; -use anyhow::{anyhow, bail, Context as _, Result}; +use anyhow::{anyhow, Context as _, Result}; use fs::{normalize_path, Fs}; use futures::future::LocalBoxFuture; use futures::{ @@ -112,7 +112,8 @@ impl WasmHost { ) -> Task> { let this = self.clone(); executor.clone().spawn(async move { - let zed_api_version = parse_wasm_extension_version(&manifest.id, &wasm_bytes)?; + let zed_api_version = + extension::parse_wasm_extension_version(&manifest.id, &wasm_bytes)?; let component = Component::from_binary(&this.engine, &wasm_bytes) .context("failed to compile wasm component")?; @@ -197,49 +198,6 @@ impl WasmHost { } } -pub fn parse_wasm_extension_version( - extension_id: &str, - wasm_bytes: &[u8], -) -> Result { - let mut version = None; - - for part in wasmparser::Parser::new(0).parse_all(wasm_bytes) { - if let wasmparser::Payload::CustomSection(s) = - part.context("error parsing wasm extension")? - { - if s.name() == "zed:api-version" { - version = parse_wasm_extension_version_custom_section(s.data()); - if version.is_none() { - bail!( - "extension {} has invalid zed:api-version section: {:?}", - extension_id, - s.data() - ); - } - } - } - } - - // The reason we wait until we're done parsing all of the Wasm bytes to return the version - // is to work around a panic that can happen inside of Wasmtime when the bytes are invalid. - // - // By parsing the entirety of the Wasm bytes before we return, we're able to detect this problem - // earlier as an `Err` rather than as a panic. - version.ok_or_else(|| anyhow!("extension {} has no zed:api-version section", extension_id)) -} - -fn parse_wasm_extension_version_custom_section(data: &[u8]) -> Option { - if data.len() == 6 { - Some(SemanticVersion::new( - u16::from_be_bytes([data[0], data[1]]) as _, - u16::from_be_bytes([data[2], data[3]]) as _, - u16::from_be_bytes([data[4], data[5]]) as _, - )) - } else { - None - } -} - impl WasmExtension { pub async fn call(&self, f: Fn) -> T where diff --git a/crates/extension/src/wasm_host/wit.rs b/crates/extension_host/src/wasm_host/wit.rs similarity index 100% rename from crates/extension/src/wasm_host/wit.rs rename to crates/extension_host/src/wasm_host/wit.rs diff --git a/crates/extension/src/wasm_host/wit/since_v0_0_1.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs similarity index 100% rename from crates/extension/src/wasm_host/wit/since_v0_0_1.rs rename to crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs diff --git a/crates/extension/src/wasm_host/wit/since_v0_0_4.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs similarity index 100% rename from crates/extension/src/wasm_host/wit/since_v0_0_4.rs rename to crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs diff --git a/crates/extension/src/wasm_host/wit/since_v0_0_6.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs similarity index 100% rename from crates/extension/src/wasm_host/wit/since_v0_0_6.rs rename to crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs diff --git a/crates/extension/src/wasm_host/wit/since_v0_1_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs similarity index 100% rename from crates/extension/src/wasm_host/wit/since_v0_1_0.rs rename to crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs diff --git a/crates/extension/src/wasm_host/wit/since_v0_2_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs similarity index 100% rename from crates/extension/src/wasm_host/wit/since_v0_2_0.rs rename to crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs diff --git a/crates/extensions_ui/Cargo.toml b/crates/extensions_ui/Cargo.toml index 28f0fcb7ad..0de1fd947a 100644 --- a/crates/extensions_ui/Cargo.toml +++ b/crates/extensions_ui/Cargo.toml @@ -20,7 +20,7 @@ client.workspace = true collections.workspace = true db.workspace = true editor.workspace = true -extension.workspace = true +extension_host.workspace = true fs.workspace = true fuzzy.workspace = true gpui.workspace = true diff --git a/crates/extensions_ui/src/extension_suggest.rs b/crates/extensions_ui/src/extension_suggest.rs index b21621537f..223a36699e 100644 --- a/crates/extensions_ui/src/extension_suggest.rs +++ b/crates/extensions_ui/src/extension_suggest.rs @@ -4,7 +4,7 @@ use std::sync::{Arc, OnceLock}; use db::kvp::KEY_VALUE_STORE; use editor::Editor; -use extension::ExtensionStore; +use extension_host::ExtensionStore; use gpui::{Model, VisualContext}; use language::Buffer; use ui::{SharedString, ViewContext}; diff --git a/crates/extensions_ui/src/extension_version_selector.rs b/crates/extensions_ui/src/extension_version_selector.rs index 23208bc710..1041e9524f 100644 --- a/crates/extensions_ui/src/extension_version_selector.rs +++ b/crates/extensions_ui/src/extension_version_selector.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use std::sync::Arc; use client::ExtensionMetadata; -use extension::{ExtensionSettings, ExtensionStore}; +use extension_host::{ExtensionSettings, ExtensionStore}; use fs::Fs; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ @@ -167,7 +167,7 @@ impl PickerDelegate for ExtensionVersionSelectorDelegate { let candidate_id = self.matches[self.selected_index].candidate_id; let extension_version = &self.extension_versions[candidate_id]; - if !extension::is_version_compatible(ReleaseChannel::global(cx), extension_version) { + if !extension_host::is_version_compatible(ReleaseChannel::global(cx), extension_version) { return; } @@ -203,7 +203,7 @@ impl PickerDelegate for ExtensionVersionSelectorDelegate { let extension_version = &self.extension_versions[version_match.candidate_id]; let is_version_compatible = - extension::is_version_compatible(ReleaseChannel::global(cx), extension_version); + extension_host::is_version_compatible(ReleaseChannel::global(cx), extension_version); let disabled = !is_version_compatible; Some( diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index f246e3cf4f..e6386ffe4a 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -11,7 +11,7 @@ use client::telemetry::Telemetry; use client::ExtensionMetadata; use collections::{BTreeMap, BTreeSet}; use editor::{Editor, EditorElement, EditorStyle}; -use extension::{ExtensionManifest, ExtensionOperation, ExtensionStore}; +use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, uniform_list, AppContext, EventEmitter, Flatten, FocusableView, InteractiveElement, @@ -203,8 +203,8 @@ impl ExtensionsPage { let subscriptions = [ cx.observe(&store, |_, _, cx| cx.notify()), cx.subscribe(&store, move |this, _, event, cx| match event { - extension::Event::ExtensionsUpdated => this.fetch_extensions_debounced(cx), - extension::Event::ExtensionInstalled(extension_id) => { + extension_host::Event::ExtensionsUpdated => this.fetch_extensions_debounced(cx), + extension_host::Event::ExtensionInstalled(extension_id) => { this.on_extension_installed(workspace_handle.clone(), extension_id, cx) } _ => {} @@ -691,7 +691,8 @@ impl ExtensionsPage { has_dev_extension: bool, cx: &mut ViewContext, ) -> (Button, Option