diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3843a3343b4a79..84ed0dd5d44ab8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -25,6 +25,7 @@ env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
+ RUSTFLAGS: "-D warnings"
jobs:
migration_checks:
@@ -91,6 +92,7 @@ jobs:
macos_tests:
timeout-minutes: 60
name: (macOS) Run Clippy and tests
+ if: github.repository_owner == 'zed-industries'
runs-on:
- self-hosted
- test
@@ -115,17 +117,18 @@ jobs:
uses: ./.github/actions/run_tests
- name: Build collab
- run: RUSTFLAGS="-D warnings" cargo build -p collab
+ run: cargo build -p collab
- name: Build other binaries and features
run: |
- RUSTFLAGS="-D warnings" cargo build --workspace --bins --all-features
+ cargo build --workspace --bins --all-features
cargo check -p gpui --features "macos-blade"
- RUSTFLAGS="-D warnings" cargo build -p remote_server
+ cargo build -p remote_server
linux_tests:
timeout-minutes: 60
name: (Linux) Run Clippy and tests
+ if: github.repository_owner == 'zed-industries'
runs-on:
- buildjet-16vcpu-ubuntu-2204
steps:
@@ -153,11 +156,12 @@ jobs:
uses: ./.github/actions/run_tests
- name: Build Zed
- run: RUSTFLAGS="-D warnings" cargo build -p zed
+ run: cargo build -p zed
build_remote_server:
timeout-minutes: 60
name: (Linux) Build Remote Server
+ if: github.repository_owner == 'zed-industries'
runs-on:
- buildjet-16vcpu-ubuntu-2204
steps:
@@ -179,14 +183,18 @@ jobs:
run: ./script/remote-server && ./script/install-mold 2.34.0
- name: Build Remote Server
- run: RUSTFLAGS="-D warnings" cargo build -p remote_server
+ run: cargo build -p remote_server
# todo(windows): Actually run the tests
windows_tests:
timeout-minutes: 60
name: (Windows) Run Clippy and tests
+ if: github.repository_owner == 'zed-industries'
runs-on: hosted-windows-1
steps:
+ # more info here:- https://github.com/rust-lang/cargo/issues/13020
+ - name: Enable longer pathnames for git
+ run: git config --system core.longpaths true
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
@@ -203,7 +211,7 @@ jobs:
run: cargo xtask clippy
- name: Build Zed
- run: $env:RUSTFLAGS="-D warnings"; cargo build
+ run: cargo build
bundle-mac:
timeout-minutes: 60
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 57e3cc7c59ff77..8d064b64f5bcac 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -1,3 +1,3 @@
# Code of Conduct
-The Code of Conduct for this repository can be found online at [zed.dev/docs/code-of-conduct](https://zed.dev/docs/code-of-conduct).
+The Code of Conduct for this repository can be found online at [zed.dev/code-of-conduct](https://zed.dev/code-of-conduct).
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f7657b9ccd4603..4a0a632413911f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,7 +2,7 @@
Thanks for your interest in contributing to Zed, the collaborative platform that is also a code editor!
-All activity in Zed forums is subject to our [Code of Conduct](https://zed.dev/docs/code-of-conduct). Additionally, contributors must sign our [Contributor License Agreement](https://zed.dev/cla) before their contributions can be merged.
+All activity in Zed forums is subject to our [Code of Conduct](https://zed.dev/code-of-conduct). Additionally, contributors must sign our [Contributor License Agreement](https://zed.dev/cla) before their contributions can be merged.
## Contribution ideas
diff --git a/Cargo.lock b/Cargo.lock
index d5f0c7cad927a5..b4a1682bc6ed44 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -16,6 +16,7 @@ dependencies = [
"project",
"smallvec",
"ui",
+ "util",
"workspace",
]
@@ -291,6 +292,12 @@ dependencies = [
"syn 2.0.76",
]
+[[package]]
+name = "arraydeque"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
+
[[package]]
name = "arrayref"
version = "0.3.8"
@@ -385,7 +392,7 @@ dependencies = [
"ctor",
"db",
"editor",
- "env_logger",
+ "env_logger 0.11.5",
"feature_flags",
"fs",
"futures 0.3.30",
@@ -847,7 +854,7 @@ dependencies = [
"chrono",
"futures-util",
"http-types",
- "hyper 0.14.30",
+ "hyper 0.14.31",
"hyper-rustls 0.24.2",
"serde",
"serde_json",
@@ -1343,7 +1350,7 @@ dependencies = [
"http-body 0.4.6",
"http-body 1.0.1",
"httparse",
- "hyper 0.14.30",
+ "hyper 0.14.31",
"hyper-rustls 0.24.2",
"once_cell",
"pin-project-lite",
@@ -1434,7 +1441,7 @@ dependencies = [
"headers",
"http 0.2.12",
"http-body 0.4.6",
- "hyper 0.14.30",
+ "hyper 0.14.31",
"itoa",
"matchit",
"memchr",
@@ -1580,7 +1587,7 @@ dependencies = [
"bitflags 2.6.0",
"cexpr",
"clang-sys",
- "itertools 0.10.5",
+ "itertools 0.12.1",
"lazy_static",
"lazycell",
"proc-macro2",
@@ -2359,7 +2366,7 @@ dependencies = [
"clickhouse-derive",
"clickhouse-rs-cityhash-sys",
"futures 0.3.30",
- "hyper 0.14.30",
+ "hyper 0.14.31",
"hyper-tls",
"lz4",
"sealed",
@@ -2551,7 +2558,7 @@ dependencies = [
"dashmap 6.0.1",
"derive_more",
"editor",
- "env_logger",
+ "env_logger 0.11.5",
"envy",
"file_finder",
"fs",
@@ -2562,7 +2569,7 @@ dependencies = [
"gpui",
"hex",
"http_client",
- "hyper 0.14.30",
+ "hyper 0.14.31",
"indoc",
"jsonwebtoken",
"language",
@@ -2706,7 +2713,7 @@ dependencies = [
"command_palette_hooks",
"ctor",
"editor",
- "env_logger",
+ "env_logger 0.11.5",
"fuzzy",
"go_to_line",
"gpui",
@@ -3572,7 +3579,7 @@ dependencies = [
"collections",
"ctor",
"editor",
- "env_logger",
+ "env_logger 0.11.5",
"futures 0.3.30",
"gpui",
"language",
@@ -3760,7 +3767,7 @@ dependencies = [
"ctor",
"db",
"emojis",
- "env_logger",
+ "env_logger 0.11.5",
"file_icons",
"futures 0.3.30",
"fuzzy",
@@ -3801,6 +3808,7 @@ dependencies = [
"tree-sitter-rust",
"tree-sitter-typescript",
"ui",
+ "unicode-segmentation",
"unindent",
"url",
"util",
@@ -3967,6 +3975,19 @@ dependencies = [
"regex",
]
+[[package]]
+name = "env_logger"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
+dependencies = [
+ "humantime",
+ "is-terminal",
+ "log",
+ "regex",
+ "termcolor",
+]
+
[[package]]
name = "env_logger"
version = "0.11.5"
@@ -4075,7 +4096,7 @@ dependencies = [
"client",
"clock",
"collections",
- "env_logger",
+ "env_logger 0.11.5",
"feature_flags",
"fs",
"git",
@@ -4170,7 +4191,7 @@ dependencies = [
"client",
"collections",
"ctor",
- "env_logger",
+ "env_logger 0.11.5",
"fs",
"futures 0.3.30",
"gpui",
@@ -4212,7 +4233,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"clap",
- "env_logger",
+ "env_logger 0.11.5",
"extension",
"fs",
"language",
@@ -4371,7 +4392,7 @@ dependencies = [
"collections",
"ctor",
"editor",
- "env_logger",
+ "env_logger 0.11.5",
"file_icons",
"futures 0.3.30",
"fuzzy",
@@ -4979,12 +5000,13 @@ dependencies = [
"git",
"gpui",
"http_client",
+ "indoc",
"pretty_assertions",
"regex",
"serde",
"serde_json",
- "unindent",
"url",
+ "util",
]
[[package]]
@@ -5126,7 +5148,7 @@ dependencies = [
"ctor",
"derive_more",
"embed-resource",
- "env_logger",
+ "env_logger 0.11.5",
"etagere",
"filedescriptor",
"flume",
@@ -5316,6 +5338,15 @@ dependencies = [
"serde",
]
+[[package]]
+name = "hashlink"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
+dependencies = [
+ "hashbrown 0.14.5",
+]
+
[[package]]
name = "hashlink"
version = "0.9.1"
@@ -5629,9 +5660,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
-version = "0.14.30"
+version = "0.14.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9"
+checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85"
dependencies = [
"bytes 1.7.2",
"futures-channel",
@@ -5679,7 +5710,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"futures-util",
"http 0.2.12",
- "hyper 0.14.30",
+ "hyper 0.14.31",
"log",
"rustls 0.21.12",
"rustls-native-certs 0.6.3",
@@ -5712,7 +5743,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes 1.7.2",
- "hyper 0.14.30",
+ "hyper 0.14.31",
"native-tls",
"tokio",
"tokio-native-tls",
@@ -6214,6 +6245,20 @@ dependencies = [
"simple_asn1",
]
+[[package]]
+name = "jupyter-serde"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a444fb3f87ee6885eb316028cc998c7d84811663ef95d78c419419423d5a054"
+dependencies = [
+ "anyhow",
+ "chrono",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "uuid",
+]
+
[[package]]
name = "khronos-egl"
version = "6.0.0"
@@ -6274,7 +6319,7 @@ dependencies = [
"collections",
"ctor",
"ec4rs",
- "env_logger",
+ "env_logger 0.11.5",
"futures 0.3.30",
"fuzzy",
"git",
@@ -6331,7 +6376,7 @@ dependencies = [
"copilot",
"ctor",
"editor",
- "env_logger",
+ "env_logger 0.11.5",
"feature_flags",
"futures 0.3.30",
"google_ai",
@@ -6388,9 +6433,10 @@ dependencies = [
"collections",
"copilot",
"editor",
- "env_logger",
+ "env_logger 0.11.5",
"futures 0.3.30",
"gpui",
+ "itertools 0.13.0",
"language",
"lsp",
"project",
@@ -6402,6 +6448,7 @@ dependencies = [
"ui",
"util",
"workspace",
+ "zed_actions",
]
[[package]]
@@ -6422,6 +6469,11 @@ dependencies = [
"lsp",
"node_runtime",
"paths",
+ "pet",
+ "pet-conda",
+ "pet-core",
+ "pet-poetry",
+ "pet-reporter",
"project",
"regex",
"rope",
@@ -6718,7 +6770,7 @@ dependencies = [
"async-pipe",
"collections",
"ctor",
- "env_logger",
+ "env_logger 0.11.5",
"futures 0.3.30",
"gpui",
"log",
@@ -6801,7 +6853,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"assets",
- "env_logger",
+ "env_logger 0.11.5",
"futures 0.3.30",
"gpui",
"language",
@@ -6914,7 +6966,7 @@ dependencies = [
"clap",
"clap_complete",
"elasticlunr-rs",
- "env_logger",
+ "env_logger 0.11.5",
"futures-util",
"handlebars 5.1.2",
"ignore",
@@ -7096,6 +7148,15 @@ dependencies = [
"windows-sys 0.48.0",
]
+[[package]]
+name = "msvc_spectre_libs"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8661ace213a0a130c7c5b9542df5023aedf092a02008ccf477b39ff108990305"
+dependencies = [
+ "cc",
+]
+
[[package]]
name = "multi_buffer"
version = "0.1.0"
@@ -7104,7 +7165,7 @@ dependencies = [
"clock",
"collections",
"ctor",
- "env_logger",
+ "env_logger 0.11.5",
"futures 0.3.30",
"gpui",
"itertools 0.13.0",
@@ -7183,6 +7244,21 @@ dependencies = [
"tempfile",
]
+[[package]]
+name = "nbformat"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "146074ad45cab20f5d98ccded164826158471f21d04f96e40b9872529e10979d"
+dependencies = [
+ "anyhow",
+ "chrono",
+ "jupyter-serde",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "uuid",
+]
+
[[package]]
name = "ndk"
version = "0.8.0"
@@ -7818,8 +7894,10 @@ dependencies = [
"serde",
"serde_json",
"settings",
+ "smallvec",
"smol",
"theme",
+ "ui",
"util",
"workspace",
"worktree",
@@ -8062,6 +8140,366 @@ dependencies = [
"sha2",
]
+[[package]]
+name = "pet"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "clap",
+ "env_logger 0.10.2",
+ "lazy_static",
+ "log",
+ "msvc_spectre_libs",
+ "pet-conda",
+ "pet-core",
+ "pet-env-var-path",
+ "pet-fs",
+ "pet-global-virtualenvs",
+ "pet-homebrew",
+ "pet-jsonrpc",
+ "pet-linux-global-python",
+ "pet-mac-commandlinetools",
+ "pet-mac-python-org",
+ "pet-mac-xcode",
+ "pet-pipenv",
+ "pet-poetry",
+ "pet-pyenv",
+ "pet-python-utils",
+ "pet-reporter",
+ "pet-telemetry",
+ "pet-venv",
+ "pet-virtualenv",
+ "pet-virtualenvwrapper",
+ "pet-windows-registry",
+ "pet-windows-store",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "pet-conda"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "env_logger 0.10.2",
+ "lazy_static",
+ "log",
+ "msvc_spectre_libs",
+ "pet-core",
+ "pet-fs",
+ "pet-python-utils",
+ "pet-reporter",
+ "regex",
+ "serde",
+ "serde_json",
+ "yaml-rust2",
+]
+
+[[package]]
+name = "pet-core"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "clap",
+ "lazy_static",
+ "log",
+ "msvc_spectre_libs",
+ "pet-fs",
+ "regex",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "pet-env-var-path"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "lazy_static",
+ "log",
+ "msvc_spectre_libs",
+ "pet-conda",
+ "pet-core",
+ "pet-fs",
+ "pet-python-utils",
+ "pet-virtualenv",
+ "regex",
+]
+
+[[package]]
+name = "pet-fs"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "log",
+ "msvc_spectre_libs",
+]
+
+[[package]]
+name = "pet-global-virtualenvs"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "log",
+ "msvc_spectre_libs",
+ "pet-conda",
+ "pet-core",
+ "pet-fs",
+ "pet-virtualenv",
+]
+
+[[package]]
+name = "pet-homebrew"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "lazy_static",
+ "log",
+ "msvc_spectre_libs",
+ "pet-conda",
+ "pet-core",
+ "pet-fs",
+ "pet-python-utils",
+ "pet-virtualenv",
+ "regex",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "pet-jsonrpc"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "env_logger 0.10.2",
+ "log",
+ "msvc_spectre_libs",
+ "pet-core",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "pet-linux-global-python"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "log",
+ "msvc_spectre_libs",
+ "pet-core",
+ "pet-fs",
+ "pet-python-utils",
+ "pet-virtualenv",
+]
+
+[[package]]
+name = "pet-mac-commandlinetools"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "log",
+ "msvc_spectre_libs",
+ "pet-core",
+ "pet-fs",
+ "pet-python-utils",
+ "pet-virtualenv",
+]
+
+[[package]]
+name = "pet-mac-python-org"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "log",
+ "msvc_spectre_libs",
+ "pet-core",
+ "pet-fs",
+ "pet-python-utils",
+ "pet-virtualenv",
+]
+
+[[package]]
+name = "pet-mac-xcode"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "log",
+ "msvc_spectre_libs",
+ "pet-core",
+ "pet-fs",
+ "pet-python-utils",
+ "pet-virtualenv",
+]
+
+[[package]]
+name = "pet-pipenv"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "log",
+ "msvc_spectre_libs",
+ "pet-core",
+ "pet-fs",
+ "pet-python-utils",
+ "pet-virtualenv",
+]
+
+[[package]]
+name = "pet-poetry"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "base64 0.22.1",
+ "lazy_static",
+ "log",
+ "msvc_spectre_libs",
+ "pet-core",
+ "pet-fs",
+ "pet-python-utils",
+ "pet-reporter",
+ "pet-virtualenv",
+ "regex",
+ "serde",
+ "serde_json",
+ "sha2",
+ "toml 0.8.19",
+]
+
+[[package]]
+name = "pet-pyenv"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "lazy_static",
+ "log",
+ "msvc_spectre_libs",
+ "pet-conda",
+ "pet-core",
+ "pet-fs",
+ "pet-python-utils",
+ "pet-reporter",
+ "regex",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "pet-python-utils"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "env_logger 0.10.2",
+ "lazy_static",
+ "log",
+ "msvc_spectre_libs",
+ "pet-core",
+ "pet-fs",
+ "regex",
+ "serde",
+ "serde_json",
+ "sha2",
+]
+
+[[package]]
+name = "pet-reporter"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "env_logger 0.10.2",
+ "log",
+ "msvc_spectre_libs",
+ "pet-core",
+ "pet-jsonrpc",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "pet-telemetry"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "env_logger 0.10.2",
+ "lazy_static",
+ "log",
+ "msvc_spectre_libs",
+ "pet-core",
+ "pet-fs",
+ "pet-python-utils",
+ "regex",
+]
+
+[[package]]
+name = "pet-venv"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "log",
+ "msvc_spectre_libs",
+ "pet-core",
+ "pet-python-utils",
+ "pet-virtualenv",
+]
+
+[[package]]
+name = "pet-virtualenv"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "log",
+ "msvc_spectre_libs",
+ "pet-core",
+ "pet-fs",
+ "pet-python-utils",
+]
+
+[[package]]
+name = "pet-virtualenvwrapper"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "log",
+ "msvc_spectre_libs",
+ "pet-core",
+ "pet-fs",
+ "pet-python-utils",
+ "pet-virtualenv",
+]
+
+[[package]]
+name = "pet-windows-registry"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "lazy_static",
+ "log",
+ "msvc_spectre_libs",
+ "pet-conda",
+ "pet-core",
+ "pet-fs",
+ "pet-python-utils",
+ "pet-virtualenv",
+ "pet-windows-store",
+ "regex",
+ "winreg 0.52.0",
+]
+
+[[package]]
+name = "pet-windows-store"
+version = "0.1.0"
+source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
+dependencies = [
+ "lazy_static",
+ "log",
+ "msvc_spectre_libs",
+ "pet-core",
+ "pet-fs",
+ "pet-python-utils",
+ "pet-virtualenv",
+ "regex",
+ "winreg 0.52.0",
+]
+
[[package]]
name = "petgraph"
version = "0.6.5"
@@ -8150,7 +8588,7 @@ dependencies = [
"anyhow",
"ctor",
"editor",
- "env_logger",
+ "env_logger 0.11.5",
"gpui",
"menu",
"serde",
@@ -8498,7 +8936,7 @@ dependencies = [
"collections",
"dap",
"dap_adapters",
- "env_logger",
+ "env_logger 0.11.5",
"fs",
"futures 0.3.30",
"fuzzy",
@@ -9211,10 +9649,11 @@ dependencies = [
"async-watch",
"backtrace",
"cargo_toml",
+ "chrono",
"clap",
"client",
"clock",
- "env_logger",
+ "env_logger 0.11.5",
"fork",
"fs",
"futures 0.3.30",
@@ -9230,6 +9669,8 @@ dependencies = [
"node_runtime",
"paths",
"project",
+ "proto",
+ "release_channel",
"remote",
"reqwest_client",
"rpc",
@@ -9239,6 +9680,7 @@ dependencies = [
"settings",
"shellexpand 2.1.2",
"smol",
+ "telemetry_events",
"toml 0.8.19",
"util",
"worktree",
@@ -9265,7 +9707,8 @@ dependencies = [
"collections",
"command_palette_hooks",
"editor",
- "env_logger",
+ "env_logger 0.11.5",
+ "feature_flags",
"futures 0.3.30",
"gpui",
"http_client",
@@ -9275,7 +9718,9 @@ dependencies = [
"languages",
"log",
"markdown_preview",
+ "menu",
"multi_buffer",
+ "nbformat",
"project",
"runtimelib",
"schemars",
@@ -9310,7 +9755,7 @@ dependencies = [
"h2 0.3.26",
"http 0.2.12",
"http-body 0.4.6",
- "hyper 0.14.30",
+ "hyper 0.14.31",
"hyper-tls",
"ipnet",
"js-sys",
@@ -9545,10 +9990,11 @@ dependencies = [
"arrayvec",
"criterion",
"ctor",
- "env_logger",
+ "env_logger 0.11.5",
"gpui",
"log",
"rand 0.8.5",
+ "rayon",
"smallvec",
"sum_tree",
"unicode-segmentation",
@@ -9576,7 +10022,7 @@ dependencies = [
"base64 0.22.1",
"chrono",
"collections",
- "env_logger",
+ "env_logger 0.11.5",
"futures 0.3.30",
"gpui",
"parking_lot",
@@ -9614,9 +10060,9 @@ dependencies = [
[[package]]
name = "runtimelib"
-version = "0.15.0"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7d76d28b882a7b889ebb04e79bc2b160b3061821ea596ff0f4a838fc7a76db0"
+checksum = "263588fe9593333c4bfde258c9021fc64e766ea434e070c6b67c7100536d6499"
dependencies = [
"anyhow",
"async-dispatcher",
@@ -9628,6 +10074,7 @@ dependencies = [
"dirs 5.0.1",
"futures 0.3.30",
"glob",
+ "jupyter-serde",
"rand 0.8.5",
"ring 0.17.8",
"serde",
@@ -10165,7 +10612,7 @@ dependencies = [
"client",
"clock",
"collections",
- "env_logger",
+ "env_logger 0.11.5",
"feature_flags",
"fs",
"futures 0.3.30",
@@ -10859,7 +11306,7 @@ dependencies = [
"futures-io",
"futures-util",
"hashbrown 0.14.5",
- "hashlink",
+ "hashlink 0.9.1",
"hex",
"indexmap 2.4.0",
"log",
@@ -11183,7 +11630,7 @@ version = "0.1.0"
dependencies = [
"arrayvec",
"ctor",
- "env_logger",
+ "env_logger 0.11.5",
"log",
"rand 0.8.5",
"rayon",
@@ -11197,7 +11644,7 @@ dependencies = [
"client",
"collections",
"editor",
- "env_logger",
+ "env_logger 0.11.5",
"futures 0.3.30",
"gpui",
"http_client",
@@ -11496,7 +11943,7 @@ dependencies = [
"collections",
"ctor",
"editor",
- "env_logger",
+ "env_logger 0.11.5",
"gpui",
"language",
"menu",
@@ -11704,7 +12151,7 @@ dependencies = [
"clock",
"collections",
"ctor",
- "env_logger",
+ "env_logger 0.11.5",
"gpui",
"http_client",
"log",
@@ -12193,6 +12640,21 @@ dependencies = [
"winnow 0.6.18",
]
+[[package]]
+name = "toolchain_selector"
+version = "0.1.0"
+dependencies = [
+ "editor",
+ "fuzzy",
+ "gpui",
+ "language",
+ "picker",
+ "project",
+ "ui",
+ "util",
+ "workspace",
+]
+
[[package]]
name = "topological-sort"
version = "0.2.2"
@@ -12938,6 +13400,7 @@ dependencies = [
"git",
"gpui",
"picker",
+ "project",
"ui",
"util",
"workspace",
@@ -13086,7 +13549,7 @@ dependencies = [
"futures-util",
"headers",
"http 0.2.12",
- "hyper 0.14.30",
+ "hyper 0.14.31",
"log",
"mime",
"mime_guess",
@@ -13799,7 +14262,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
- "windows-sys 0.48.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -14361,7 +14824,7 @@ dependencies = [
"collections",
"db",
"derive_more",
- "env_logger",
+ "env_logger 0.11.5",
"fs",
"futures 0.3.30",
"git",
@@ -14398,12 +14861,13 @@ dependencies = [
"anyhow",
"clock",
"collections",
- "env_logger",
+ "env_logger 0.11.5",
"fs",
"futures 0.3.30",
"fuzzy",
"git",
"git2",
+ "git_hosting_providers",
"gpui",
"http_client",
"ignore",
@@ -14568,6 +15032,17 @@ dependencies = [
"clap",
]
+[[package]]
+name = "yaml-rust2"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8"
+dependencies = [
+ "arraydeque",
+ "encoding_rs",
+ "hashlink 0.8.4",
+]
+
[[package]]
name = "yansi"
version = "1.0.1"
@@ -14655,7 +15130,7 @@ dependencies = [
[[package]]
name = "zed"
-version = "0.160.0"
+version = "0.161.0"
dependencies = [
"activity_indicator",
"anyhow",
@@ -14683,7 +15158,7 @@ dependencies = [
"debugger_ui",
"diagnostics",
"editor",
- "env_logger",
+ "env_logger 0.11.5",
"extension",
"extensions_ui",
"feature_flags",
@@ -14722,6 +15197,7 @@ dependencies = [
"project",
"project_panel",
"project_symbols",
+ "proto",
"quick_action_bar",
"recent_projects",
"release_channel",
@@ -14750,6 +15226,7 @@ dependencies = [
"theme",
"theme_selector",
"time",
+ "toolchain_selector",
"tree-sitter-md",
"tree-sitter-rust",
"ui",
@@ -14795,13 +15272,6 @@ dependencies = [
"zed_extension_api 0.1.0",
]
-[[package]]
-name = "zed_dart"
-version = "0.1.1"
-dependencies = [
- "zed_extension_api 0.1.0",
-]
-
[[package]]
name = "zed_deno"
version = "0.0.2"
diff --git a/Cargo.toml b/Cargo.toml
index 3faa3da8c90ece..4d583f10ca64a6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -120,6 +120,7 @@ members = [
"crates/theme_selector",
"crates/time_format",
"crates/title_bar",
+ "crates/toolchain_selector",
"crates/ui",
"crates/ui_input",
"crates/ui_macros",
@@ -140,7 +141,6 @@ members = [
"extensions/astro",
"extensions/clojure",
"extensions/csharp",
- "extensions/dart",
"extensions/deno",
"extensions/elixir",
"extensions/elm",
@@ -298,6 +298,7 @@ theme_importer = { path = "crates/theme_importer" }
theme_selector = { path = "crates/theme_selector" }
time_format = { path = "crates/time_format" }
title_bar = { path = "crates/title_bar" }
+toolchain_selector = { path = "crates/toolchain_selector" }
ui = { path = "crates/ui" }
ui_input = { path = "crates/ui_input" }
ui_macros = { path = "crates/ui_macros" }
@@ -377,6 +378,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"
nix = "0.29"
num-format = "0.4.4"
once_cell = "1.19.0"
@@ -384,6 +386,11 @@ ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
pathdiff = "0.2"
+pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
+pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
+pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
+pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
+pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
postage = { version = "0.5", features = ["futures-traits"] }
pretty_assertions = "1.3.0"
profiling = "1"
@@ -392,6 +399,7 @@ prost-build = "0.9"
prost-types = "0.9"
pulldown-cmark = { version = "0.12.0", default-features = false }
rand = "0.8.5"
+rayon = "1.8"
regex = "1.5"
repair_json = "0.1.0"
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f6998da16bbca97b6dddda9be7827c50e29", default-features = false, features = [
@@ -403,7 +411,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f
"stream",
] }
rsa = "0.9.6"
-runtimelib = { version = "0.15", default-features = false, features = [
+runtimelib = { version = "0.16.0", default-features = false, features = [
"async-dispatcher-runtime",
] }
rustc-demangle = "0.1.23"
diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json
index a9fe4a2eff59b6..fe293256b393cc 100644
--- a/assets/icons/file_icons/file_types.json
+++ b/assets/icons/file_icons/file_types.json
@@ -58,6 +58,7 @@
"gitignore": "vcs",
"gitkeep": "vcs",
"gitmodules": "vcs",
+ "gleam": "gleam",
"go": "go",
"gql": "graphql",
"graphql": "graphql",
@@ -83,6 +84,7 @@
"j2k": "image",
"java": "java",
"jfif": "image",
+ "jl": "julia",
"jp2": "image",
"jpeg": "image",
"jpg": "image",
@@ -90,7 +92,6 @@
"json": "storage",
"jsonc": "storage",
"jsx": "react",
- "julia": "julia",
"jxl": "image",
"kt": "kotlin",
"ldf": "storage",
@@ -264,6 +265,9 @@
"fsharp": {
"icon": "icons/file_icons/fsharp.svg"
},
+ "gleam": {
+ "icon": "icons/file_icons/gleam.svg"
+ },
"go": {
"icon": "icons/file_icons/go.svg"
},
diff --git a/assets/icons/file_icons/gleam.svg b/assets/icons/file_icons/gleam.svg
new file mode 100644
index 00000000000000..6a3dc2c96fe76b
--- /dev/null
+++ b/assets/icons/file_icons/gleam.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/icons/list_x.svg b/assets/icons/list_x.svg
new file mode 100644
index 00000000000000..683f38ab5dfe5b
--- /dev/null
+++ b/assets/icons/list_x.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json
index 4f55fa9772b4db..0ba76fba3f6265 100644
--- a/assets/keymaps/default-linux.json
+++ b/assets/keymaps/default-linux.json
@@ -532,6 +532,7 @@
"context": "ContextEditor > Editor",
"bindings": {
"ctrl-enter": "assistant::Assist",
+ "ctrl-shift-enter": "assistant::Edit",
"ctrl-s": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",
diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json
index ade3ece1eda930..964af3ce3d3c06 100644
--- a/assets/keymaps/default-macos.json
+++ b/assets/keymaps/default-macos.json
@@ -201,6 +201,7 @@
"context": "ContextEditor > Editor",
"bindings": {
"cmd-enter": "assistant::Assist",
+ "cmd-shift-enter": "assistant::Edit",
"cmd-s": "workspace::Save",
"cmd->": "assistant::QuoteSelection",
"cmd-<": "assistant::InsertIntoEditor",
@@ -349,6 +350,7 @@
"alt-cmd-]": "editor::UnfoldLines",
"cmd-k cmd-l": "editor::ToggleFold",
"cmd-k cmd-[": "editor::FoldRecursive",
+ "cmd-k cmd-]": "editor::UnfoldRecursive",
"cmd-k cmd-1": ["editor::FoldAtLevel", { "level": 1 }],
"cmd-k cmd-2": ["editor::FoldAtLevel", { "level": 2 }],
"cmd-k cmd-3": ["editor::FoldAtLevel", { "level": 3 }],
diff --git a/assets/prompts/edit_workflow.hbs b/assets/prompts/suggest_edits.hbs
similarity index 100%
rename from assets/prompts/edit_workflow.hbs
rename to assets/prompts/suggest_edits.hbs
diff --git a/assets/settings/default.json b/assets/settings/default.json
index 04bd67643436c7..565e959cf4bb5f 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -346,8 +346,6 @@
"git_status": true,
// Amount of indentation for nested items.
"indent_size": 20,
- // Whether to show indent guides in the project panel.
- "indent_guides": true,
// Whether to reveal it in the project panel automatically,
// when a corresponding project entry becomes active.
// Gitignored entries are never auto revealed.
@@ -371,6 +369,17 @@
/// 5. Never show the scrollbar:
/// "never"
"show": null
+ },
+ // Settings related to indent guides in the project panel.
+ "indent_guides": {
+ // When to show indent guides in the project panel.
+ // This setting can take two values:
+ //
+ // 1. Always show indent guides:
+ // "always"
+ // 2. Never show indent guides:
+ // "never"
+ "show": "always"
}
},
"outline_panel": {
@@ -394,7 +403,35 @@
"auto_reveal_entries": true,
/// Whether to fold directories automatically
/// when a directory has only one directory inside.
- "auto_fold_dirs": true
+ "auto_fold_dirs": true,
+ // Settings related to indent guides in the outline panel.
+ "indent_guides": {
+ // When to show indent guides in the outline panel.
+ // This setting can take two values:
+ //
+ // 1. Always show indent guides:
+ // "always"
+ // 2. Never show indent guides:
+ // "never"
+ "show": "always"
+ },
+ /// Scrollbar-related settings
+ "scrollbar": {
+ /// When to show the scrollbar in the project panel.
+ /// This setting can take four values:
+ ///
+ /// 1. null (default): Inherit editor settings
+ /// 2. Show the scrollbar if there's important information or
+ /// follow the system's configured behavior (default):
+ /// "auto"
+ /// 3. Match the system's configured behavior:
+ /// "system"
+ /// 4. Always show the scrollbar:
+ /// "always"
+ /// 5. Never show the scrollbar:
+ /// "never"
+ "show": null
+ }
},
"collaboration_panel": {
// Whether to show the collaboration panel button in the status bar.
@@ -777,6 +814,7 @@
"tasks": {
"variables": {}
},
+ "toolchain": { "name": "default", "path": "default" },
// An object whose keys are language names, and whose values
// are arrays of filenames or extensions of files that should
// use those languages.
diff --git a/crates/activity_indicator/Cargo.toml b/crates/activity_indicator/Cargo.toml
index 9761a082385ac7..b4fb2ec5b089ae 100644
--- a/crates/activity_indicator/Cargo.toml
+++ b/crates/activity_indicator/Cargo.toml
@@ -23,6 +23,7 @@ language.workspace = true
project.workspace = true
smallvec.workspace = true
ui.workspace = true
+util.workspace = true
workspace.workspace = true
[dev-dependencies]
diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs
index e2fb516f88f7b6..4959b1192dead5 100644
--- a/crates/activity_indicator/src/activity_indicator.rs
+++ b/crates/activity_indicator/src/activity_indicator.rs
@@ -13,7 +13,8 @@ use language::{
use project::{EnvironmentErrorMessage, LanguageServerProgress, Project, WorktreeId};
use smallvec::SmallVec;
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
-use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle};
+use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip};
+use util::truncate_and_trailoff;
use workspace::{item::ItemHandle, StatusItemView, Workspace};
actions!(activity_indicator, [ShowErrorMessage]);
@@ -463,6 +464,8 @@ impl ActivityIndicator {
impl EventEmitter for ActivityIndicator {}
+const MAX_MESSAGE_LEN: usize = 50;
+
impl Render for ActivityIndicator {
fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement {
let result = h_flex()
@@ -473,6 +476,7 @@ impl Render for ActivityIndicator {
return result;
};
let this = cx.view().downgrade();
+ let truncate_content = content.message.len() > MAX_MESSAGE_LEN;
result.gap_2().child(
PopoverMenu::new("activity-indicator-popover")
.trigger(
@@ -481,7 +485,21 @@ impl Render for ActivityIndicator {
.id("activity-indicator-status")
.gap_2()
.children(content.icon)
- .child(Label::new(content.message).size(LabelSize::Small))
+ .map(|button| {
+ if truncate_content {
+ button
+ .child(
+ Label::new(truncate_and_trailoff(
+ &content.message,
+ MAX_MESSAGE_LEN,
+ ))
+ .size(LabelSize::Small),
+ )
+ .tooltip(move |cx| Tooltip::text(&content.message, cx))
+ } else {
+ button.child(Label::new(content.message).size(LabelSize::Small))
+ }
+ })
.when_some(content.on_click, |this, handler| {
this.on_click(cx.listener(move |this, _, cx| {
handler(this, cx);
diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs
index e1e574744fff61..c2857d06d437f7 100644
--- a/crates/assistant/src/assistant.rs
+++ b/crates/assistant/src/assistant.rs
@@ -41,12 +41,10 @@ use prompts::PromptLoadingParams;
use semantic_index::{CloudEmbeddingProvider, SemanticDb};
use serde::{Deserialize, Serialize};
use settings::{update_settings_file, Settings, SettingsStore};
-use slash_command::workflow_command::WorkflowSlashCommand;
use slash_command::{
auto_command, cargo_workspace_command, context_server_command, default_command, delta_command,
diagnostics_command, docs_command, fetch_command, file_command, now_command, project_command,
prompt_command, search_command, symbols_command, tab_command, terminal_command,
- workflow_command,
};
use std::path::PathBuf;
use std::sync::Arc;
@@ -59,6 +57,7 @@ actions!(
assistant,
[
Assist,
+ Edit,
Split,
CopyCode,
CycleMessageRole,
@@ -298,25 +297,64 @@ fn register_context_server_handlers(cx: &mut AppContext) {
return;
};
- if let Some(prompts) = protocol.list_prompts().await.log_err() {
- for prompt in prompts
- .into_iter()
- .filter(context_server_command::acceptable_prompt)
- {
- log::info!(
- "registering context server command: {:?}",
- prompt.name
- );
- context_server_registry.register_command(
- server.id.clone(),
- prompt.name.as_str(),
- );
- slash_command_registry.register_command(
- context_server_command::ContextServerSlashCommand::new(
- &server, prompt,
- ),
- true,
- );
+ if protocol.capable(context_servers::protocol::ServerCapability::Prompts) {
+ if let Some(prompts) = protocol.list_prompts().await.log_err() {
+ for prompt in prompts
+ .into_iter()
+ .filter(context_server_command::acceptable_prompt)
+ {
+ log::info!(
+ "registering context server command: {:?}",
+ prompt.name
+ );
+ context_server_registry.register_command(
+ server.id.clone(),
+ prompt.name.as_str(),
+ );
+ slash_command_registry.register_command(
+ context_server_command::ContextServerSlashCommand::new(
+ &server, prompt,
+ ),
+ true,
+ );
+ }
+ }
+ }
+ })
+ .detach();
+ }
+ },
+ );
+
+ cx.update_model(
+ &manager,
+ |manager: &mut context_servers::manager::ContextServerManager, cx| {
+ let tool_registry = ToolRegistry::global(cx);
+ let context_server_registry = ContextServerRegistry::global(cx);
+ if let Some(server) = manager.get_server(server_id) {
+ cx.spawn(|_, _| async move {
+ let Some(protocol) = server.client.read().clone() else {
+ return;
+ };
+
+ if protocol.capable(context_servers::protocol::ServerCapability::Tools) {
+ if let Some(tools) = protocol.list_tools().await.log_err() {
+ for tool in tools.tools {
+ log::info!(
+ "registering context server tool: {:?}",
+ tool.name
+ );
+ context_server_registry.register_tool(
+ server.id.clone(),
+ tool.name.as_str(),
+ );
+ tool_registry.register_tool(
+ tools::context_server_tool::ContextServerTool::new(
+ server.id.clone(),
+ tool
+ ),
+ );
+ }
}
}
})
@@ -334,6 +372,14 @@ fn register_context_server_handlers(cx: &mut AppContext) {
context_server_registry.unregister_command(&server_id, &command_name);
}
}
+
+ if let Some(tools) = context_server_registry.get_tools(server_id) {
+ let tool_registry = ToolRegistry::global(cx);
+ for tool_name in tools {
+ tool_registry.unregister_tool_by_name(&tool_name);
+ context_server_registry.unregister_tool(&server_id, &tool_name);
+ }
+ }
}
},
)
@@ -397,22 +443,6 @@ fn register_slash_commands(prompt_builder: Option>, cx: &mut
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
if let Some(prompt_builder) = prompt_builder {
- cx.observe_global::({
- let slash_command_registry = slash_command_registry.clone();
- let prompt_builder = prompt_builder.clone();
- move |cx| {
- if AssistantSettings::get_global(cx).are_live_diffs_enabled(cx) {
- slash_command_registry.register_command(
- workflow_command::WorkflowSlashCommand::new(prompt_builder.clone()),
- true,
- );
- } else {
- slash_command_registry.unregister_command_by_name(WorkflowSlashCommand::NAME);
- }
- }
- })
- .detach();
-
cx.observe_flag::({
let slash_command_registry = slash_command_registry.clone();
move |is_enabled, _cx| {
diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs
index 42857406976c2b..eef82c610681f0 100644
--- a/crates/assistant/src/assistant_panel.rs
+++ b/crates/assistant/src/assistant_panel.rs
@@ -13,10 +13,11 @@ use crate::{
terminal_inline_assistant::TerminalInlineAssistant,
Assist, AssistantPatch, AssistantPatchStatus, CacheStatus, ConfirmCommand, Content, Context,
ContextEvent, ContextId, ContextStore, ContextStoreEvent, CopyCode, CycleMessageRole,
- DeployHistory, DeployPromptLibrary, InlineAssistant, InsertDraggedFiles, InsertIntoEditor,
- Message, MessageId, MessageMetadata, MessageStatus, ModelPickerDelegate, ModelSelector,
- NewContext, PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection,
- RemoteContextMetadata, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
+ DeployHistory, DeployPromptLibrary, Edit, InlineAssistant, InsertDraggedFiles,
+ InsertIntoEditor, Message, MessageId, MessageMetadata, MessageStatus, ModelPickerDelegate,
+ ModelSelector, NewContext, PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection,
+ RemoteContextMetadata, RequestType, SavedContextMetadata, Split, ToggleFocus,
+ ToggleModelSelector,
};
use anyhow::Result;
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
@@ -1461,6 +1462,7 @@ type MessageHeader = MessageMetadata;
#[derive(Clone)]
enum AssistError {
+ FileRequired,
PaymentRequired,
MaxMonthlySpendReached,
Message(SharedString),
@@ -1588,23 +1590,11 @@ impl ContextEditor {
}
fn assist(&mut self, _: &Assist, cx: &mut ViewContext) {
- let provider = LanguageModelRegistry::read_global(cx).active_provider();
- if provider
- .as_ref()
- .map_or(false, |provider| provider.must_accept_terms(cx))
- {
- self.show_accept_terms = true;
- cx.notify();
- return;
- }
-
- if self.focus_active_patch(cx) {
- return;
- }
+ self.send_to_model(RequestType::Chat, cx);
+ }
- self.last_error = None;
- self.send_to_model(cx);
- cx.notify();
+ fn edit(&mut self, _: &Edit, cx: &mut ViewContext) {
+ self.send_to_model(RequestType::SuggestEdits, cx);
}
fn focus_active_patch(&mut self, cx: &mut ViewContext) -> bool {
@@ -1622,8 +1612,30 @@ impl ContextEditor {
false
}
- fn send_to_model(&mut self, cx: &mut ViewContext) {
- if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) {
+ fn send_to_model(&mut self, request_type: RequestType, cx: &mut ViewContext) {
+ let provider = LanguageModelRegistry::read_global(cx).active_provider();
+ if provider
+ .as_ref()
+ .map_or(false, |provider| provider.must_accept_terms(cx))
+ {
+ self.show_accept_terms = true;
+ cx.notify();
+ return;
+ }
+
+ if self.focus_active_patch(cx) {
+ return;
+ }
+
+ self.last_error = None;
+
+ if request_type == RequestType::SuggestEdits && !self.context.read(cx).contains_files(cx) {
+ self.last_error = Some(AssistError::FileRequired);
+ cx.notify();
+ } else if let Some(user_message) = self
+ .context
+ .update(cx, |context, cx| context.assist(request_type, cx))
+ {
let new_selection = {
let cursor = user_message
.start
@@ -1640,6 +1652,8 @@ impl ContextEditor {
// Avoid scrolling to the new cursor position so the assistant's output is stable.
cx.defer(|this, _| this.scroll_position = None);
}
+
+ cx.notify();
}
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext) {
@@ -1667,8 +1681,10 @@ impl ContextEditor {
});
}
- fn cursors(&self, cx: &AppContext) -> Vec {
- let selections = self.editor.read(cx).selections.all::(cx);
+ fn cursors(&self, cx: &mut WindowContext) -> Vec {
+ let selections = self
+ .editor
+ .update(cx, |editor, cx| editor.selections.all::(cx));
selections
.into_iter()
.map(|selection| selection.head())
@@ -2375,7 +2391,9 @@ impl ContextEditor {
}
fn update_active_patch(&mut self, cx: &mut ViewContext) {
- let newest_cursor = self.editor.read(cx).selections.newest::(cx).head();
+ let newest_cursor = self.editor.update(cx, |editor, cx| {
+ editor.selections.newest::(cx).head()
+ });
let context = self.context.read(cx);
let new_patch = context.patch_containing(newest_cursor, cx).cloned();
@@ -2782,39 +2800,40 @@ impl ContextEditor {
) -> Option<(String, bool)> {
const CODE_FENCE_DELIMITER: &'static str = "```";
- let context_editor = context_editor_view.read(cx).editor.read(cx);
-
- if context_editor.selections.newest::(cx).is_empty() {
- let snapshot = context_editor.buffer().read(cx).snapshot(cx);
- let (_, _, snapshot) = snapshot.as_singleton()?;
-
- let head = context_editor.selections.newest::(cx).head();
- let offset = snapshot.point_to_offset(head);
+ let context_editor = context_editor_view.read(cx).editor.clone();
+ context_editor.update(cx, |context_editor, cx| {
+ if context_editor.selections.newest::(cx).is_empty() {
+ let snapshot = context_editor.buffer().read(cx).snapshot(cx);
+ let (_, _, snapshot) = snapshot.as_singleton()?;
+
+ let head = context_editor.selections.newest::(cx).head();
+ let offset = snapshot.point_to_offset(head);
+
+ let surrounding_code_block_range = find_surrounding_code_block(snapshot, offset)?;
+ let mut text = snapshot
+ .text_for_range(surrounding_code_block_range)
+ .collect::();
+
+ // If there is no newline trailing the closing three-backticks, then
+ // tree-sitter-md extends the range of the content node to include
+ // the backticks.
+ if text.ends_with(CODE_FENCE_DELIMITER) {
+ text.drain((text.len() - CODE_FENCE_DELIMITER.len())..);
+ }
- let surrounding_code_block_range = find_surrounding_code_block(snapshot, offset)?;
- let mut text = snapshot
- .text_for_range(surrounding_code_block_range)
- .collect::();
+ (!text.is_empty()).then_some((text, true))
+ } else {
+ let anchor = context_editor.selections.newest_anchor();
+ let text = context_editor
+ .buffer()
+ .read(cx)
+ .read(cx)
+ .text_for_range(anchor.range())
+ .collect::();
- // If there is no newline trailing the closing three-backticks, then
- // tree-sitter-md extends the range of the content node to include
- // the backticks.
- if text.ends_with(CODE_FENCE_DELIMITER) {
- text.drain((text.len() - CODE_FENCE_DELIMITER.len())..);
+ (!text.is_empty()).then_some((text, false))
}
-
- (!text.is_empty()).then_some((text, true))
- } else {
- let anchor = context_editor.selections.newest_anchor();
- let text = context_editor
- .buffer()
- .read(cx)
- .read(cx)
- .text_for_range(anchor.range())
- .collect::();
-
- (!text.is_empty()).then_some((text, false))
- }
+ })
}
fn insert_selection(
@@ -3644,7 +3663,13 @@ impl ContextEditor {
button.tooltip(move |_| tooltip.clone())
})
.layer(ElevationIndex::ModalSurface)
- .child(Label::new("Send"))
+ .child(Label::new(
+ if AssistantSettings::get_global(cx).are_live_diffs_enabled(cx) {
+ "Chat"
+ } else {
+ "Send"
+ },
+ ))
.children(
KeyBinding::for_action_in(&Assist, &focus_handle, cx)
.map(|binding| binding.into_any_element()),
@@ -3654,6 +3679,57 @@ impl ContextEditor {
})
}
+ fn render_edit_button(&self, cx: &mut ViewContext) -> impl IntoElement {
+ let focus_handle = self.focus_handle(cx).clone();
+
+ let (style, tooltip) = match token_state(&self.context, cx) {
+ Some(TokenState::NoTokensLeft { .. }) => (
+ ButtonStyle::Tinted(TintColor::Negative),
+ Some(Tooltip::text("Token limit reached", cx)),
+ ),
+ Some(TokenState::HasMoreTokens {
+ over_warn_threshold,
+ ..
+ }) => {
+ let (style, tooltip) = if over_warn_threshold {
+ (
+ ButtonStyle::Tinted(TintColor::Warning),
+ Some(Tooltip::text("Token limit is close to exhaustion", cx)),
+ )
+ } else {
+ (ButtonStyle::Filled, None)
+ };
+ (style, tooltip)
+ }
+ None => (ButtonStyle::Filled, None),
+ };
+
+ let provider = LanguageModelRegistry::read_global(cx).active_provider();
+
+ let has_configuration_error = configuration_error(cx).is_some();
+ let needs_to_accept_terms = self.show_accept_terms
+ && provider
+ .as_ref()
+ .map_or(false, |provider| provider.must_accept_terms(cx));
+ let disabled = has_configuration_error || needs_to_accept_terms;
+
+ ButtonLike::new("edit_button")
+ .disabled(disabled)
+ .style(style)
+ .when_some(tooltip, |button, tooltip| {
+ button.tooltip(move |_| tooltip.clone())
+ })
+ .layer(ElevationIndex::ModalSurface)
+ .child(Label::new("Suggest Edits"))
+ .children(
+ KeyBinding::for_action_in(&Edit, &focus_handle, cx)
+ .map(|binding| binding.into_any_element()),
+ )
+ .on_click(move |_event, cx| {
+ focus_handle.dispatch_action(&Edit, cx);
+ })
+ }
+
fn render_last_error(&self, cx: &mut ViewContext) -> Option {
let last_error = self.last_error.as_ref()?;
@@ -3668,6 +3744,7 @@ impl ContextEditor {
.elevation_2(cx)
.occlude()
.child(match last_error {
+ AssistError::FileRequired => self.render_file_required_error(cx),
AssistError::PaymentRequired => self.render_payment_required_error(cx),
AssistError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(cx)
@@ -3680,6 +3757,41 @@ impl ContextEditor {
)
}
+ fn render_file_required_error(&self, cx: &mut ViewContext) -> AnyElement {
+ v_flex()
+ .gap_0p5()
+ .child(
+ h_flex()
+ .gap_1p5()
+ .items_center()
+ .child(Icon::new(IconName::Warning).color(Color::Warning))
+ .child(
+ Label::new("Suggest Edits needs a file to edit").weight(FontWeight::MEDIUM),
+ ),
+ )
+ .child(
+ div()
+ .id("error-message")
+ .max_h_24()
+ .overflow_y_scroll()
+ .child(Label::new(
+ "To include files, type /file or /tab in your prompt.",
+ )),
+ )
+ .child(
+ h_flex()
+ .justify_end()
+ .mt_1()
+ .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
+ |this, _, cx| {
+ this.last_error = None;
+ cx.notify();
+ },
+ ))),
+ )
+ .into_any()
+ }
+
fn render_payment_required_error(&self, cx: &mut ViewContext) -> AnyElement {
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
@@ -3910,6 +4022,7 @@ impl Render for ContextEditor {
.capture_action(cx.listener(ContextEditor::paste))
.capture_action(cx.listener(ContextEditor::cycle_message_role))
.capture_action(cx.listener(ContextEditor::confirm_command))
+ .on_action(cx.listener(ContextEditor::edit))
.on_action(cx.listener(ContextEditor::assist))
.on_action(cx.listener(ContextEditor::split))
.size_full()
@@ -3974,7 +4087,21 @@ impl Render for ContextEditor {
h_flex()
.w_full()
.justify_end()
- .child(div().child(self.render_send_button(cx))),
+ .when(
+ AssistantSettings::get_global(cx).are_live_diffs_enabled(cx),
+ |buttons| {
+ buttons
+ .items_center()
+ .gap_1p5()
+ .child(self.render_edit_button(cx))
+ .child(
+ Label::new("or")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ },
+ )
+ .child(self.render_send_button(cx)),
),
),
)
@@ -4707,7 +4834,7 @@ impl Render for ConfigurationView {
let mut element = v_flex()
.id("assistant-configuration-view")
- .track_focus(&self.focus_handle)
+ .track_focus(&self.focus_handle(cx))
.bg(cx.theme().colors().editor_background)
.size_full()
.overflow_y_scroll()
diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs
index 78237e51b21656..a1de9d3b4069a1 100644
--- a/crates/assistant/src/context.rs
+++ b/crates/assistant/src/context.rs
@@ -2,8 +2,9 @@
mod context_tests;
use crate::{
- prompts::PromptBuilder, slash_command::SlashCommandLine, AssistantEdit, AssistantPatch,
- AssistantPatchStatus, MessageId, MessageStatus,
+ prompts::PromptBuilder,
+ slash_command::{file_command::FileCommandMetadata, SlashCommandLine},
+ AssistantEdit, AssistantPatch, AssistantPatchStatus, MessageId, MessageStatus,
};
use anyhow::{anyhow, Context as _, Result};
use assistant_slash_command::{
@@ -66,6 +67,14 @@ impl ContextId {
}
}
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum RequestType {
+ /// Request a normal chat response from the model.
+ Chat,
+ /// Add a preamble to the message, which tells the model to return a structured response that suggests edits.
+ SuggestEdits,
+}
+
#[derive(Clone, Debug)]
pub enum ContextOperation {
InsertMessage {
@@ -981,6 +990,20 @@ impl Context {
&self.slash_command_output_sections
}
+ pub fn contains_files(&self, cx: &AppContext) -> bool {
+ let buffer = self.buffer.read(cx);
+ self.slash_command_output_sections.iter().any(|section| {
+ section.is_valid(buffer)
+ && section
+ .metadata
+ .as_ref()
+ .and_then(|metadata| {
+ serde_json::from_value::(metadata.clone()).ok()
+ })
+ .is_some()
+ })
+ }
+
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
self.pending_tool_uses_by_id.values().collect()
}
@@ -1028,7 +1051,7 @@ impl Context {
}
pub(crate) fn count_remaining_tokens(&mut self, cx: &mut ModelContext) {
- let request = self.to_completion_request(cx);
+ let request = self.to_completion_request(RequestType::SuggestEdits, cx); // Conservatively assume SuggestEdits, since it takes more tokens.
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return;
};
@@ -1171,7 +1194,7 @@ impl Context {
}
let request = {
- let mut req = self.to_completion_request(cx);
+ let mut req = self.to_completion_request(RequestType::Chat, cx);
// Skip the last message because it's likely to change and
// therefore would be a waste to cache.
req.messages.pop();
@@ -1859,7 +1882,11 @@ impl Context {
})
}
- pub fn assist(&mut self, cx: &mut ModelContext) -> Option {
+ pub fn assist(
+ &mut self,
+ request_type: RequestType,
+ cx: &mut ModelContext,
+ ) -> Option {
let model_registry = LanguageModelRegistry::read_global(cx);
let provider = model_registry.active_provider()?;
let model = model_registry.active_model()?;
@@ -1872,7 +1899,7 @@ impl Context {
// Compute which messages to cache, including the last one.
self.mark_cache_anchors(&model.cache_configuration(), false, cx);
- let mut request = self.to_completion_request(cx);
+ let mut request = self.to_completion_request(request_type, cx);
if cx.has_flag::() {
let tool_registry = ToolRegistry::global(cx);
@@ -2074,7 +2101,11 @@ impl Context {
Some(user_message)
}
- pub fn to_completion_request(&self, cx: &AppContext) -> LanguageModelRequest {
+ pub fn to_completion_request(
+ &self,
+ request_type: RequestType,
+ cx: &AppContext,
+ ) -> LanguageModelRequest {
let buffer = self.buffer.read(cx);
let mut contents = self.contents(cx).peekable();
@@ -2163,6 +2194,25 @@ impl Context {
completion_request.messages.push(request_message);
}
+ if let RequestType::SuggestEdits = request_type {
+ if let Ok(preamble) = self.prompt_builder.generate_workflow_prompt() {
+ let last_elem_index = completion_request.messages.len();
+
+ completion_request
+ .messages
+ .push(LanguageModelRequestMessage {
+ role: Role::User,
+ content: vec![MessageContent::Text(preamble)],
+ cache: false,
+ });
+
+ // The preamble message should be sent right before the last actual user message.
+ completion_request
+ .messages
+ .swap(last_elem_index, last_elem_index.saturating_sub(1));
+ }
+ }
+
completion_request
}
@@ -2477,7 +2527,7 @@ impl Context {
return;
}
- let mut request = self.to_completion_request(cx);
+ let mut request = self.to_completion_request(RequestType::Chat, cx);
request.messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![
diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs
index 9af8193605f00f..fdf00c8b044484 100644
--- a/crates/assistant/src/inline_assistant.rs
+++ b/crates/assistant/src/inline_assistant.rs
@@ -1,7 +1,7 @@
use crate::{
assistant_settings::AssistantSettings, humanize_token_count, prompts::PromptBuilder,
AssistantPanel, AssistantPanelEvent, CharOperation, CycleNextInlineAssist,
- CyclePreviousInlineAssist, LineDiff, LineOperation, ModelSelector, StreamingDiff,
+ CyclePreviousInlineAssist, LineDiff, LineOperation, ModelSelector, RequestType, StreamingDiff,
};
use anyhow::{anyhow, Context as _, Result};
use client::{telemetry::Telemetry, ErrorExt};
@@ -189,11 +189,16 @@ impl InlineAssistant {
initial_prompt: Option,
cx: &mut WindowContext,
) {
- let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
+ let (snapshot, initial_selections) = editor.update(cx, |editor, cx| {
+ (
+ editor.buffer().read(cx).snapshot(cx),
+ editor.selections.all::(cx),
+ )
+ });
let mut selections = Vec::>::new();
let mut newest_selection = None;
- for mut selection in editor.read(cx).selections.all::(cx) {
+ for mut selection in initial_selections {
if selection.end > selection.start {
selection.start.column = 0;
// If the selection ends at the start of the line, we don't want to include it.
@@ -566,10 +571,13 @@ impl InlineAssistant {
return;
};
- let editor = editor.read(cx);
- if editor.selections.count() == 1 {
- let selection = editor.selections.newest::(cx);
- let buffer = editor.buffer().read(cx).snapshot(cx);
+ if editor.read(cx).selections.count() == 1 {
+ let (selection, buffer) = editor.update(cx, |editor, cx| {
+ (
+ editor.selections.newest::(cx),
+ editor.buffer().read(cx).snapshot(cx),
+ )
+ });
for assist_id in &editor_assists.assist_ids {
let assist = &self.assists[assist_id];
let assist_range = assist.range.to_offset(&buffer);
@@ -594,10 +602,13 @@ impl InlineAssistant {
return;
};
- let editor = editor.read(cx);
- if editor.selections.count() == 1 {
- let selection = editor.selections.newest::(cx);
- let buffer = editor.buffer().read(cx).snapshot(cx);
+ if editor.read(cx).selections.count() == 1 {
+ let (selection, buffer) = editor.update(cx, |editor, cx| {
+ (
+ editor.selections.newest::(cx),
+ editor.buffer().read(cx).snapshot(cx),
+ )
+ });
let mut closest_assist_fallback = None;
for assist_id in &editor_assists.assist_ids {
let assist = &self.assists[assist_id];
@@ -2234,7 +2245,7 @@ impl InlineAssist {
.read(cx)
.active_context(cx)?
.read(cx)
- .to_completion_request(cx),
+ .to_completion_request(RequestType::Chat, cx),
)
} else {
None
diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs
index 2d0829086c8bfc..50fee242eab42d 100644
--- a/crates/assistant/src/prompts.rs
+++ b/crates/assistant/src/prompts.rs
@@ -311,7 +311,7 @@ impl PromptBuilder {
}
pub fn generate_workflow_prompt(&self) -> Result {
- self.handlebars.lock().render("edit_workflow", &())
+ self.handlebars.lock().render("suggest_edits", &())
}
pub fn generate_project_slash_command_prompt(
diff --git a/crates/assistant/src/slash_command.rs b/crates/assistant/src/slash_command.rs
index e430e35622a222..ed20791d9560ed 100644
--- a/crates/assistant/src/slash_command.rs
+++ b/crates/assistant/src/slash_command.rs
@@ -34,7 +34,6 @@ pub mod search_command;
pub mod symbols_command;
pub mod tab_command;
pub mod terminal_command;
-pub mod workflow_command;
pub(crate) struct SlashCommandCompletionProvider {
cancel_flag: Mutex>,
diff --git a/crates/assistant/src/slash_command/workflow_command.rs b/crates/assistant/src/slash_command/workflow_command.rs
deleted file mode 100644
index ca6ccde92ee0c7..00000000000000
--- a/crates/assistant/src/slash_command/workflow_command.rs
+++ /dev/null
@@ -1,82 +0,0 @@
-use std::sync::atomic::AtomicBool;
-use std::sync::Arc;
-
-use anyhow::Result;
-use assistant_slash_command::{
- ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
- SlashCommandResult,
-};
-use gpui::{Task, WeakView};
-use language::{BufferSnapshot, LspAdapterDelegate};
-use ui::prelude::*;
-use workspace::Workspace;
-
-use crate::prompts::PromptBuilder;
-
-pub(crate) struct WorkflowSlashCommand {
- prompt_builder: Arc,
-}
-
-impl WorkflowSlashCommand {
- pub const NAME: &'static str = "workflow";
-
- pub fn new(prompt_builder: Arc) -> Self {
- Self { prompt_builder }
- }
-}
-
-impl SlashCommand for WorkflowSlashCommand {
- fn name(&self) -> String {
- Self::NAME.into()
- }
-
- fn description(&self) -> String {
- "Insert prompt to opt into the edit workflow".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 prompt_builder = self.prompt_builder.clone();
- cx.spawn(|_cx| async move {
- let text = prompt_builder.generate_workflow_prompt()?;
- let range = 0..text.len();
-
- Ok(SlashCommandOutput {
- text,
- sections: vec![SlashCommandOutputSection {
- range,
- icon: IconName::Route,
- label: "Workflow".into(),
- metadata: None,
- }],
- run_commands_in_text: false,
- }
- .to_event_stream())
- })
- }
-}
diff --git a/crates/assistant/src/terminal_inline_assistant.rs b/crates/assistant/src/terminal_inline_assistant.rs
index 41b8d9eb88ac25..3e472ae4a97fb4 100644
--- a/crates/assistant/src/terminal_inline_assistant.rs
+++ b/crates/assistant/src/terminal_inline_assistant.rs
@@ -1,6 +1,6 @@
use crate::{
humanize_token_count, prompts::PromptBuilder, AssistantPanel, AssistantPanelEvent,
- ModelSelector, DEFAULT_CONTEXT_LINES,
+ ModelSelector, RequestType, DEFAULT_CONTEXT_LINES,
};
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
@@ -251,7 +251,7 @@ impl TerminalInlineAssistant {
.read(cx)
.active_context(cx)?
.read(cx)
- .to_completion_request(cx),
+ .to_completion_request(RequestType::Chat, cx),
)
})
} else {
diff --git a/crates/assistant/src/tools.rs b/crates/assistant/src/tools.rs
index abde04e760e3ee..83a396c0203cb2 100644
--- a/crates/assistant/src/tools.rs
+++ b/crates/assistant/src/tools.rs
@@ -1 +1,2 @@
+pub mod context_server_tool;
pub mod now_tool;
diff --git a/crates/assistant/src/tools/context_server_tool.rs b/crates/assistant/src/tools/context_server_tool.rs
new file mode 100644
index 00000000000000..93edb32b75b725
--- /dev/null
+++ b/crates/assistant/src/tools/context_server_tool.rs
@@ -0,0 +1,82 @@
+use anyhow::{anyhow, bail};
+use assistant_tool::Tool;
+use context_servers::manager::ContextServerManager;
+use context_servers::types;
+use gpui::Task;
+
+pub struct ContextServerTool {
+ server_id: String,
+ tool: types::Tool,
+}
+
+impl ContextServerTool {
+ pub fn new(server_id: impl Into, tool: types::Tool) -> Self {
+ Self {
+ server_id: server_id.into(),
+ tool,
+ }
+ }
+}
+
+impl Tool for ContextServerTool {
+ fn name(&self) -> String {
+ self.tool.name.clone()
+ }
+
+ fn description(&self) -> String {
+ self.tool.description.clone().unwrap_or_default()
+ }
+
+ fn input_schema(&self) -> serde_json::Value {
+ match &self.tool.input_schema {
+ serde_json::Value::Null => {
+ serde_json::json!({ "type": "object", "properties": [] })
+ }
+ serde_json::Value::Object(map) if map.is_empty() => {
+ serde_json::json!({ "type": "object", "properties": [] })
+ }
+ _ => self.tool.input_schema.clone(),
+ }
+ }
+
+ fn run(
+ self: std::sync::Arc,
+ input: serde_json::Value,
+ _workspace: gpui::WeakView,
+ cx: &mut ui::WindowContext,
+ ) -> gpui::Task> {
+ let manager = ContextServerManager::global(cx);
+ let manager = manager.read(cx);
+ if let Some(server) = manager.get_server(&self.server_id) {
+ cx.foreground_executor().spawn({
+ let tool_name = self.tool.name.clone();
+ async move {
+ let Some(protocol) = server.client.read().clone() else {
+ bail!("Context server not initialized");
+ };
+
+ let arguments = if let serde_json::Value::Object(map) = input {
+ Some(map.into_iter().collect())
+ } else {
+ None
+ };
+
+ log::trace!(
+ "Running tool: {} with arguments: {:?}",
+ tool_name,
+ arguments
+ );
+ let response = protocol.run_tool(tool_name, arguments).await?;
+
+ let tool_result = match response.tool_result {
+ serde_json::Value::String(s) => s,
+ _ => serde_json::to_string(&response.tool_result)?,
+ };
+ Ok(tool_result)
+ }
+ })
+ } else {
+ Task::ready(Err(anyhow!("Context server not found")))
+ }
+ }
+}
diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs
index 61154cb5043eb8..fbbd23907a7153 100644
--- a/crates/auto_update/src/auto_update.rs
+++ b/crates/auto_update/src/auto_update.rs
@@ -84,9 +84,9 @@ pub struct AutoUpdater {
}
#[derive(Deserialize)]
-struct JsonRelease {
- version: String,
- url: String,
+pub struct JsonRelease {
+ pub version: String,
+ pub url: String,
}
struct MacOsUnmounter {
@@ -482,7 +482,7 @@ impl AutoUpdater {
release_channel: ReleaseChannel,
version: Option,
cx: &mut AsyncAppContext,
- ) -> Result<(String, String)> {
+ ) -> Result<(JsonRelease, String)> {
let this = cx.update(|cx| {
cx.default_global::()
.0
@@ -504,7 +504,7 @@ impl AutoUpdater {
let update_request_body = build_remote_server_update_request_body(cx)?;
let body = serde_json::to_string(&update_request_body)?;
- Ok((release.url, body))
+ Ok((release, body))
}
async fn get_release(
diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs
index fc5b12cfae1c39..d627d8fe15a988 100644
--- a/crates/channel/src/channel_store.rs
+++ b/crates/channel/src/channel_store.rs
@@ -3,7 +3,7 @@ mod channel_index;
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage};
use anyhow::{anyhow, Result};
use channel_index::ChannelIndex;
-use client::{ChannelId, Client, ClientSettings, ProjectId, Subscription, User, UserId, UserStore};
+use client::{ChannelId, Client, ClientSettings, Subscription, User, UserId, UserStore};
use collections::{hash_map, HashMap, HashSet};
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
use gpui::{
@@ -33,30 +33,11 @@ struct NotesVersion {
version: clock::Global,
}
-#[derive(Debug, Clone)]
-pub struct HostedProject {
- project_id: ProjectId,
- channel_id: ChannelId,
- name: SharedString,
- _visibility: proto::ChannelVisibility,
-}
-impl From for HostedProject {
- fn from(project: proto::HostedProject) -> Self {
- Self {
- project_id: ProjectId(project.project_id),
- channel_id: ChannelId(project.channel_id),
- _visibility: project.visibility(),
- name: project.name.into(),
- }
- }
-}
pub struct ChannelStore {
pub channel_index: ChannelIndex,
channel_invitations: Vec>,
channel_participants: HashMap>>,
channel_states: HashMap,
- hosted_projects: HashMap,
-
outgoing_invites: HashSet<(ChannelId, UserId)>,
update_channels_tx: mpsc::UnboundedSender,
opened_buffers: HashMap>,
@@ -85,7 +66,6 @@ pub struct ChannelState {
observed_notes_version: NotesVersion,
observed_chat_message: Option,
role: Option,
- projects: HashSet,
}
impl Channel {
@@ -216,7 +196,6 @@ impl ChannelStore {
channel_invitations: Vec::default(),
channel_index: ChannelIndex::default(),
channel_participants: Default::default(),
- hosted_projects: Default::default(),
outgoing_invites: Default::default(),
opened_buffers: Default::default(),
opened_chats: Default::default(),
@@ -316,19 +295,6 @@ impl ChannelStore {
self.channel_index.by_id().get(&channel_id)
}
- pub fn projects_for_id(&self, channel_id: ChannelId) -> Vec<(SharedString, ProjectId)> {
- let mut projects: Vec<(SharedString, ProjectId)> = self
- .channel_states
- .get(&channel_id)
- .map(|state| state.projects.clone())
- .unwrap_or_default()
- .into_iter()
- .flat_map(|id| Some((self.hosted_projects.get(&id)?.name.clone(), id)))
- .collect();
- projects.sort();
- projects
- }
-
pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &AppContext) -> bool {
if let Some(buffer) = self.opened_buffers.get(&channel_id) {
if let OpenedModelHandle::Open(buffer) = buffer {
@@ -1102,9 +1068,7 @@ impl ChannelStore {
let channels_changed = !payload.channels.is_empty()
|| !payload.delete_channels.is_empty()
|| !payload.latest_channel_message_ids.is_empty()
- || !payload.latest_channel_buffer_versions.is_empty()
- || !payload.hosted_projects.is_empty()
- || !payload.deleted_hosted_projects.is_empty();
+ || !payload.latest_channel_buffer_versions.is_empty();
if channels_changed {
if !payload.delete_channels.is_empty() {
@@ -1161,34 +1125,6 @@ impl ChannelStore {
.or_default()
.update_latest_message_id(latest_channel_message.message_id);
}
-
- for hosted_project in payload.hosted_projects {
- let hosted_project: HostedProject = hosted_project.into();
- if let Some(old_project) = self
- .hosted_projects
- .insert(hosted_project.project_id, hosted_project.clone())
- {
- self.channel_states
- .entry(old_project.channel_id)
- .or_default()
- .remove_hosted_project(old_project.project_id);
- }
- self.channel_states
- .entry(hosted_project.channel_id)
- .or_default()
- .add_hosted_project(hosted_project.project_id);
- }
-
- for hosted_project_id in payload.deleted_hosted_projects {
- let hosted_project_id = ProjectId(hosted_project_id);
-
- if let Some(old_project) = self.hosted_projects.remove(&hosted_project_id) {
- self.channel_states
- .entry(old_project.channel_id)
- .or_default()
- .remove_hosted_project(old_project.project_id);
- }
- }
}
cx.notify();
@@ -1295,12 +1231,4 @@ impl ChannelState {
};
}
}
-
- fn add_hosted_project(&mut self, project_id: ProjectId) {
- self.projects.insert(project_id);
- }
-
- fn remove_hosted_project(&mut self, project_id: ProjectId) {
- self.projects.remove(&project_id);
- }
}
diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs
index f6ee279dc83220..fab5687c418cf9 100644
--- a/crates/client/src/user.rs
+++ b/crates/client/src/user.rs
@@ -48,6 +48,7 @@ pub struct Collaborator {
pub peer_id: proto::PeerId,
pub replica_id: ReplicaId,
pub user_id: UserId,
+ pub is_host: bool,
}
impl PartialOrd for User {
@@ -824,6 +825,7 @@ impl Collaborator {
peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
replica_id: message.replica_id as ReplicaId,
user_id: message.user_id as UserId,
+ is_host: message.is_host,
})
}
}
diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql
index c6bd87a8a57156..c59091d66d0e27 100644
--- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql
+++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql
@@ -52,9 +52,7 @@ CREATE TABLE "projects" (
"host_user_id" INTEGER REFERENCES users (id),
"host_connection_id" INTEGER,
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
- "unregistered" BOOLEAN NOT NULL DEFAULT FALSE,
- "hosted_project_id" INTEGER REFERENCES hosted_projects (id),
- "dev_server_project_id" INTEGER REFERENCES dev_server_projects(id)
+ "unregistered" BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id");
CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id");
@@ -399,30 +397,6 @@ CREATE TABLE rate_buckets (
);
CREATE INDEX idx_user_id_rate_limit ON rate_buckets (user_id, rate_limit_name);
-CREATE TABLE hosted_projects (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- channel_id INTEGER NOT NULL REFERENCES channels(id),
- name TEXT NOT NULL,
- visibility TEXT NOT NULL,
- deleted_at TIMESTAMP NULL
-);
-CREATE INDEX idx_hosted_projects_on_channel_id ON hosted_projects (channel_id);
-CREATE UNIQUE INDEX uix_hosted_projects_on_channel_id_and_name ON hosted_projects (channel_id, name) WHERE (deleted_at IS NULL);
-
-CREATE TABLE dev_servers (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- user_id INTEGER NOT NULL REFERENCES users(id),
- name TEXT NOT NULL,
- ssh_connection_string TEXT,
- hashed_token TEXT NOT NULL
-);
-
-CREATE TABLE dev_server_projects (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- dev_server_id INTEGER NOT NULL REFERENCES dev_servers(id),
- paths TEXT NOT NULL
-);
-
CREATE TABLE IF NOT EXISTS billing_preferences (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
diff --git a/crates/collab/migrations/20241023201725_remove_dev_servers.sql b/crates/collab/migrations/20241023201725_remove_dev_servers.sql
new file mode 100644
index 00000000000000..c5da673a29b1e0
--- /dev/null
+++ b/crates/collab/migrations/20241023201725_remove_dev_servers.sql
@@ -0,0 +1,6 @@
+ALTER TABLE projects DROP COLUMN dev_server_project_id;
+ALTER TABLE projects DROP COLUMN hosted_project_id;
+
+DROP TABLE hosted_projects;
+DROP TABLE dev_server_projects;
+DROP TABLE dev_servers;
diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs
index 9c02e0c801c826..81db7158e83ab7 100644
--- a/crates/collab/src/db.rs
+++ b/crates/collab/src/db.rs
@@ -617,7 +617,6 @@ pub struct ChannelsForUser {
pub channels: Vec,
pub channel_memberships: Vec,
pub channel_participants: HashMap>,
- pub hosted_projects: Vec,
pub invited_channels: Vec,
pub observed_buffer_versions: Vec,
@@ -741,6 +740,7 @@ impl ProjectCollaborator {
peer_id: Some(self.connection_id.into()),
replica_id: self.replica_id.0 as u32,
user_id: self.user_id.to_proto(),
+ is_host: self.is_host,
}
}
}
diff --git a/crates/collab/src/db/queries.rs b/crates/collab/src/db/queries.rs
index 79523444ab2760..bfcd111e3f4861 100644
--- a/crates/collab/src/db/queries.rs
+++ b/crates/collab/src/db/queries.rs
@@ -10,7 +10,6 @@ pub mod contacts;
pub mod contributors;
pub mod embeddings;
pub mod extensions;
-pub mod hosted_projects;
pub mod messages;
pub mod notifications;
pub mod processed_stripe_events;
diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs
index 06ad2b45946511..dee4d820e86ff7 100644
--- a/crates/collab/src/db/queries/buffers.rs
+++ b/crates/collab/src/db/queries/buffers.rs
@@ -116,6 +116,7 @@ impl Database {
peer_id: Some(collaborator.connection().into()),
user_id: collaborator.user_id.to_proto(),
replica_id: collaborator.replica_id.0 as u32,
+ is_host: false,
})
.collect(),
})
@@ -222,6 +223,7 @@ impl Database {
peer_id: Some(collaborator.connection().into()),
user_id: collaborator.user_id.to_proto(),
replica_id: collaborator.replica_id.0 as u32,
+ is_host: false,
})
.collect(),
},
@@ -257,6 +259,7 @@ impl Database {
peer_id: Some(db_collaborator.connection().into()),
replica_id: db_collaborator.replica_id.0 as u32,
user_id: db_collaborator.user_id.to_proto(),
+ is_host: false,
})
} else {
collaborator_ids_to_remove.push(db_collaborator.id);
@@ -385,6 +388,7 @@ impl Database {
peer_id: Some(connection.into()),
replica_id: row.replica_id.0 as u32,
user_id: row.user_id.to_proto(),
+ is_host: false,
});
}
diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs
index f9da0187fec7a0..10120ea8143010 100644
--- a/crates/collab/src/db/queries/channels.rs
+++ b/crates/collab/src/db/queries/channels.rs
@@ -615,15 +615,10 @@ impl Database {
.observed_channel_messages(&channel_ids, user_id, tx)
.await?;
- let hosted_projects = self
- .get_hosted_projects(&channel_ids, &roles_by_channel_id, tx)
- .await?;
-
Ok(ChannelsForUser {
channel_memberships,
channels,
invited_channels,
- hosted_projects,
channel_participants,
latest_buffer_versions,
latest_channel_messages,
diff --git a/crates/collab/src/db/queries/hosted_projects.rs b/crates/collab/src/db/queries/hosted_projects.rs
deleted file mode 100644
index eb38eaa9ccac9b..00000000000000
--- a/crates/collab/src/db/queries/hosted_projects.rs
+++ /dev/null
@@ -1,85 +0,0 @@
-use rpc::{proto, ErrorCode};
-
-use super::*;
-
-impl Database {
- pub async fn get_hosted_projects(
- &self,
- channel_ids: &[ChannelId],
- roles: &HashMap,
- tx: &DatabaseTransaction,
- ) -> Result> {
- let projects = hosted_project::Entity::find()
- .find_also_related(project::Entity)
- .filter(hosted_project::Column::ChannelId.is_in(channel_ids.iter().map(|id| id.0)))
- .all(tx)
- .await?
- .into_iter()
- .flat_map(|(hosted_project, project)| {
- if hosted_project.deleted_at.is_some() {
- return None;
- }
- match hosted_project.visibility {
- ChannelVisibility::Public => {}
- ChannelVisibility::Members => {
- let is_visible = roles
- .get(&hosted_project.channel_id)
- .map(|role| role.can_see_all_descendants())
- .unwrap_or(false);
- if !is_visible {
- return None;
- }
- }
- };
- Some(proto::HostedProject {
- project_id: project?.id.to_proto(),
- channel_id: hosted_project.channel_id.to_proto(),
- name: hosted_project.name.clone(),
- visibility: hosted_project.visibility.into(),
- })
- })
- .collect();
-
- Ok(projects)
- }
-
- pub async fn get_hosted_project(
- &self,
- hosted_project_id: HostedProjectId,
- user_id: UserId,
- tx: &DatabaseTransaction,
- ) -> Result<(hosted_project::Model, ChannelRole)> {
- let project = hosted_project::Entity::find_by_id(hosted_project_id)
- .one(tx)
- .await?
- .ok_or_else(|| anyhow!(ErrorCode::NoSuchProject))?;
- let channel = channel::Entity::find_by_id(project.channel_id)
- .one(tx)
- .await?
- .ok_or_else(|| anyhow!(ErrorCode::NoSuchChannel))?;
-
- let role = match project.visibility {
- ChannelVisibility::Public => {
- self.check_user_is_channel_participant(&channel, user_id, tx)
- .await?
- }
- ChannelVisibility::Members => {
- self.check_user_is_channel_member(&channel, user_id, tx)
- .await?
- }
- };
-
- Ok((project, role))
- }
-
- pub async fn is_hosted_project(&self, project_id: ProjectId) -> Result {
- self.transaction(|tx| async move {
- Ok(project::Entity::find_by_id(project_id)
- .one(&*tx)
- .await?
- .map(|project| project.hosted_project_id.is_some())
- .ok_or_else(|| anyhow!(ErrorCode::NoSuchProject))?)
- })
- .await
- }
-}
diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs
index 27bec21ca1cddd..7ff8aa7a9fbb1f 100644
--- a/crates/collab/src/db/queries/projects.rs
+++ b/crates/collab/src/db/queries/projects.rs
@@ -68,7 +68,6 @@ impl Database {
connection.owner_id as i32,
))),
id: ActiveValue::NotSet,
- hosted_project_id: ActiveValue::Set(None),
}
.insert(&*tx)
.await?;
@@ -536,39 +535,6 @@ impl Database {
.await
}
- /// Adds the given connection to the specified hosted project
- pub async fn join_hosted_project(
- &self,
- id: ProjectId,
- user_id: UserId,
- connection: ConnectionId,
- ) -> Result<(Project, ReplicaId)> {
- self.transaction(|tx| async move {
- let (project, hosted_project) = project::Entity::find_by_id(id)
- .find_also_related(hosted_project::Entity)
- .one(&*tx)
- .await?
- .ok_or_else(|| anyhow!("hosted project is no longer shared"))?;
-
- let Some(hosted_project) = hosted_project else {
- return Err(anyhow!("project is not hosted"))?;
- };
-
- let channel = channel::Entity::find_by_id(hosted_project.channel_id)
- .one(&*tx)
- .await?
- .ok_or_else(|| anyhow!("no such channel"))?;
-
- let role = self
- .check_user_is_channel_participant(&channel, user_id, &tx)
- .await?;
-
- self.join_project_internal(project, user_id, connection, role, &tx)
- .await
- })
- .await
- }
-
pub async fn get_project(&self, id: ProjectId) -> Result {
self.transaction(|tx| async move {
Ok(project::Entity::find_by_id(id)
@@ -784,49 +750,6 @@ impl Database {
Ok((project, replica_id as ReplicaId))
}
- pub async fn leave_hosted_project(
- &self,
- project_id: ProjectId,
- connection: ConnectionId,
- ) -> Result {
- self.transaction(|tx| async move {
- let result = project_collaborator::Entity::delete_many()
- .filter(
- Condition::all()
- .add(project_collaborator::Column::ProjectId.eq(project_id))
- .add(project_collaborator::Column::ConnectionId.eq(connection.id as i32))
- .add(
- project_collaborator::Column::ConnectionServerId
- .eq(connection.owner_id as i32),
- ),
- )
- .exec(&*tx)
- .await?;
- if result.rows_affected == 0 {
- return Err(anyhow!("not in the project"))?;
- }
-
- let project = project::Entity::find_by_id(project_id)
- .one(&*tx)
- .await?
- .ok_or_else(|| anyhow!("no such project"))?;
- let collaborators = project
- .find_related(project_collaborator::Entity)
- .all(&*tx)
- .await?;
- let connection_ids = collaborators
- .into_iter()
- .map(|collaborator| collaborator.connection())
- .collect();
- Ok(LeftProject {
- id: project.id,
- connection_ids,
- should_unshare: false,
- })
- })
- .await
- }
-
/// Removes the given connection from the specified project.
pub async fn leave_project(
&self,
diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs
index 23dced800b56ba..8a4ec29998ac86 100644
--- a/crates/collab/src/db/tables.rs
+++ b/crates/collab/src/db/tables.rs
@@ -18,7 +18,6 @@ pub mod extension;
pub mod extension_version;
pub mod feature_flag;
pub mod follower;
-pub mod hosted_project;
pub mod language_server;
pub mod notification;
pub mod notification_kind;
diff --git a/crates/collab/src/db/tables/hosted_project.rs b/crates/collab/src/db/tables/hosted_project.rs
deleted file mode 100644
index dd7cb1b5b107f9..00000000000000
--- a/crates/collab/src/db/tables/hosted_project.rs
+++ /dev/null
@@ -1,27 +0,0 @@
-use crate::db::{ChannelId, ChannelVisibility, HostedProjectId};
-use sea_orm::entity::prelude::*;
-
-#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
-#[sea_orm(table_name = "hosted_projects")]
-pub struct Model {
- #[sea_orm(primary_key)]
- pub id: HostedProjectId,
- pub channel_id: ChannelId,
- pub name: String,
- pub visibility: ChannelVisibility,
- pub deleted_at: Option,
-}
-
-impl ActiveModelBehavior for ActiveModel {}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {
- #[sea_orm(has_one = "super::project::Entity")]
- Project,
-}
-
-impl Related for Entity {
- fn to() -> RelationDef {
- Relation::Project.def()
- }
-}
diff --git a/crates/collab/src/db/tables/project.rs b/crates/collab/src/db/tables/project.rs
index a357634aff614c..10e3da50e1dd09 100644
--- a/crates/collab/src/db/tables/project.rs
+++ b/crates/collab/src/db/tables/project.rs
@@ -1,4 +1,4 @@
-use crate::db::{HostedProjectId, ProjectId, Result, RoomId, ServerId, UserId};
+use crate::db::{ProjectId, Result, RoomId, ServerId, UserId};
use anyhow::anyhow;
use rpc::ConnectionId;
use sea_orm::entity::prelude::*;
@@ -12,7 +12,6 @@ pub struct Model {
pub host_user_id: Option,
pub host_connection_id: Option,
pub host_connection_server_id: Option,
- pub hosted_project_id: Option,
}
impl Model {
@@ -50,12 +49,6 @@ pub enum Relation {
Collaborators,
#[sea_orm(has_many = "super::language_server::Entity")]
LanguageServers,
- #[sea_orm(
- belongs_to = "super::hosted_project::Entity",
- from = "Column::HostedProjectId",
- to = "super::hosted_project::Column::Id"
- )]
- HostedProject,
}
impl Related for Entity {
@@ -88,10 +81,4 @@ impl Related for Entity {
}
}
-impl Related for Entity {
- fn to() -> RelationDef {
- Relation::HostedProject.def()
- }
-}
-
impl ActiveModelBehavior for ActiveModel {}
diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs
index adc571580a0724..9575ed505b5b11 100644
--- a/crates/collab/src/db/tests/buffer_tests.rs
+++ b/crates/collab/src/db/tests/buffer_tests.rs
@@ -121,11 +121,13 @@ async fn test_channel_buffers(db: &Arc) {
user_id: a_id.to_proto(),
peer_id: Some(rpc::proto::PeerId { id: 1, owner_id }),
replica_id: 0,
+ is_host: false,
},
rpc::proto::Collaborator {
user_id: b_id.to_proto(),
peer_id: Some(rpc::proto::PeerId { id: 2, owner_id }),
replica_id: 1,
+ is_host: false,
}
]
);
diff --git a/crates/collab/src/llm.rs b/crates/collab/src/llm.rs
index cb3478879e1490..654327c4637ad2 100644
--- a/crates/collab/src/llm.rs
+++ b/crates/collab/src/llm.rs
@@ -449,6 +449,10 @@ async fn check_usage_limit(
model_name: &str,
claims: &LlmTokenClaims,
) -> Result<()> {
+ if claims.is_staff {
+ return Ok(());
+ }
+
let model = state.db.model(provider, model_name)?;
let usage = state
.db
@@ -513,11 +517,6 @@ async fn check_usage_limit(
];
for (used, limit, usage_measure) in checks {
- // Temporarily bypass rate-limiting for staff members.
- if claims.is_staff {
- continue;
- }
-
if used > limit {
let resource = match usage_measure {
UsageMeasure::RequestsPerMinute => "requests_per_minute",
diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs
index 90277242f1b1c6..f83bebbbb1f566 100644
--- a/crates/collab/src/rpc.rs
+++ b/crates/collab/src/rpc.rs
@@ -287,7 +287,6 @@ impl Server {
.add_request_handler(share_project)
.add_message_handler(unshare_project)
.add_request_handler(join_project)
- .add_request_handler(join_hosted_project)
.add_message_handler(leave_project)
.add_request_handler(update_project)
.add_request_handler(update_worktree)
@@ -308,6 +307,8 @@ impl Server {
.add_request_handler(forward_read_only_project_request::)
.add_request_handler(forward_read_only_project_request::)
.add_request_handler(forward_read_only_project_request::)
+ .add_request_handler(forward_read_only_project_request::)
+ .add_request_handler(forward_mutating_project_request::)
.add_request_handler(forward_mutating_project_request::)
.add_request_handler(
forward_mutating_project_request::,
@@ -1793,11 +1794,6 @@ impl JoinProjectInternalResponse for Response {
Response::::send(self, result)
}
}
-impl JoinProjectInternalResponse for Response {
- fn send(self, result: proto::JoinProjectResponse) -> Result<()> {
- Response::::send(self, result)
- }
-}
fn join_project_internal(
response: impl JoinProjectInternalResponse,
@@ -1831,6 +1827,7 @@ fn join_project_internal(
peer_id: Some(session.connection_id.into()),
replica_id: replica_id.0 as u32,
user_id: guest_user_id.to_proto(),
+ is_host: false,
}),
};
@@ -1921,11 +1918,6 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
let sender_id = session.connection_id;
let project_id = ProjectId::from_proto(request.project_id);
let db = session.db().await;
- if db.is_hosted_project(project_id).await? {
- let project = db.leave_hosted_project(project_id, sender_id).await?;
- project_left(&project, &session);
- return Ok(());
- }
let (room, project) = &*db.leave_project(project_id, sender_id).await?;
tracing::info!(
@@ -1941,24 +1933,6 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
Ok(())
}
-async fn join_hosted_project(
- request: proto::JoinHostedProject,
- response: Response,
- session: Session,
-) -> Result<()> {
- let (mut project, replica_id) = session
- .db()
- .await
- .join_hosted_project(
- ProjectId(request.project_id as i32),
- session.user_id(),
- session.connection_id,
- )
- .await?;
-
- join_project_internal(response, session, &mut project, &replica_id)
-}
-
/// Updates other participants with changes to the project
async fn update_project(
request: proto::UpdateProject,
@@ -4200,7 +4174,6 @@ fn build_channels_update(channels: ChannelsForUser) -> proto::UpdateChannels {
update.channel_invitations.push(channel.to_proto());
}
- update.hosted_projects = channels.hosted_projects;
update
}
diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs
index 2a3c643f6deeb5..beb1ef61ef9886 100644
--- a/crates/collab/src/tests/editor_tests.rs
+++ b/crates/collab/src/tests/editor_tests.rs
@@ -1978,6 +1978,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
enabled: false,
delay_ms: None,
min_column: None,
+ show_commit_summary: false,
});
cx_a.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs
index 1367bf49c008e1..d708194f58396b 100644
--- a/crates/collab/src/tests/following_tests.rs
+++ b/crates/collab/src/tests/following_tests.rs
@@ -1957,9 +1957,10 @@ async fn test_following_to_channel_notes_without_a_shared_project(
});
channel_notes_1_b.update(cx_b, |notes, cx| {
assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
- let editor = notes.editor.read(cx);
- assert_eq!(editor.text(cx), "Hello from A.");
- assert_eq!(editor.selections.ranges::(cx), &[3..4]);
+ notes.editor.update(cx, |editor, cx| {
+ assert_eq!(editor.text(cx), "Hello from A.");
+ assert_eq!(editor.selections.ranges::(cx), &[3..4]);
+ })
});
// Client A opens the notes for channel 2.
diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs
index 80cc2500f5f4ca..b1e8e5686109a9 100644
--- a/crates/collab/src/tests/integration_tests.rs
+++ b/crates/collab/src/tests/integration_tests.rs
@@ -21,8 +21,8 @@ use language::{
language_settings::{
AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter,
},
- tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig,
- LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
+ tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticEntry, FakeLspAdapter,
+ Language, LanguageConfig, LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
};
use live_kit_client::MacOSDisplay;
use lsp::LanguageServerId;
@@ -4461,7 +4461,7 @@ async fn test_prettier_formatting_buffer(
},
..Default::default()
},
- Some(tree_sitter_rust::LANGUAGE.into()),
+ Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
)));
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
"TypeScript",
@@ -6575,3 +6575,95 @@ async fn test_context_collaboration_with_reconnect(
assert!(context.buffer().read(cx).read_only());
});
}
+
+#[gpui::test]
+async fn test_remote_git_branches(
+ executor: BackgroundExecutor,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ let mut server = TestServer::start(executor.clone()).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+
+ client_a
+ .fs()
+ .insert_tree("/project", serde_json::json!({ ".git":{} }))
+ .await;
+ let branches = ["main", "dev", "feature-1"];
+ client_a
+ .fs()
+ .insert_branches(Path::new("/project/.git"), &branches);
+
+ let (project_a, worktree_id) = client_a.build_local_project("/project", cx_a).await;
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+ let project_b = client_b.join_remote_project(project_id, cx_b).await;
+
+ let root_path = ProjectPath::root_path(worktree_id);
+ // Client A sees that a guest has joined.
+ executor.run_until_parked();
+
+ let branches_b = cx_b
+ .update(|cx| project_b.update(cx, |project, cx| project.branches(root_path.clone(), cx)))
+ .await
+ .unwrap();
+
+ let new_branch = branches[2];
+
+ let branches_b = branches_b
+ .into_iter()
+ .map(|branch| branch.name)
+ .collect::>();
+
+ assert_eq!(&branches_b, &branches);
+
+ cx_b.update(|cx| {
+ project_b.update(cx, |project, cx| {
+ project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
+ })
+ })
+ .await
+ .unwrap();
+
+ executor.run_until_parked();
+
+ let host_branch = cx_a.update(|cx| {
+ project_a.update(cx, |project, cx| {
+ project.worktree_store().update(cx, |worktree_store, cx| {
+ worktree_store
+ .current_branch(root_path.clone(), cx)
+ .unwrap()
+ })
+ })
+ });
+
+ assert_eq!(host_branch.as_ref(), branches[2]);
+
+ // Also try creating a new branch
+ cx_b.update(|cx| {
+ project_b.update(cx, |project, cx| {
+ project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
+ })
+ })
+ .await
+ .unwrap();
+
+ executor.run_until_parked();
+
+ let host_branch = cx_a.update(|cx| {
+ project_a.update(cx, |project, cx| {
+ project.worktree_store().update(cx, |worktree_store, cx| {
+ worktree_store.current_branch(root_path, cx).unwrap()
+ })
+ })
+ });
+
+ assert_eq!(host_branch.as_ref(), "totally-new-branch");
+}
diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs
index 0e13c88d9464ea..0e29bd5ef3c912 100644
--- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs
+++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs
@@ -1,14 +1,27 @@
use crate::tests::TestServer;
use call::ActiveCall;
+use collections::HashSet;
use fs::{FakeFs, Fs as _};
-use gpui::{Context as _, TestAppContext};
+use futures::StreamExt as _;
+use gpui::{BackgroundExecutor, Context as _, TestAppContext, UpdateGlobal as _};
use http_client::BlockedHttpClient;
-use language::{language_settings::language_settings, LanguageRegistry};
+use language::{
+ language_settings::{
+ language_settings, AllLanguageSettings, Formatter, FormatterList, PrettierSettings,
+ SelectedFormatter,
+ },
+ tree_sitter_typescript, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher,
+ LanguageRegistry,
+};
use node_runtime::NodeRuntime;
-use project::ProjectPath;
+use project::{
+ lsp_store::{FormatTarget, FormatTrigger},
+ ProjectPath,
+};
use remote::SshRemoteClient;
use remote_server::{HeadlessAppState, HeadlessProject};
use serde_json::json;
+use settings::SettingsStore;
use std::{path::Path, sync::Arc};
#[gpui::test(iterations = 10)]
@@ -174,3 +187,311 @@ async fn test_sharing_an_ssh_remote_project(
);
});
}
+
+#[gpui::test]
+async fn test_ssh_collaboration_git_branches(
+ executor: BackgroundExecutor,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+ server_cx: &mut TestAppContext,
+) {
+ cx_a.set_name("a");
+ cx_b.set_name("b");
+ server_cx.set_name("server");
+
+ let mut server = TestServer::start(executor.clone()).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+
+ // Set up project on remote FS
+ let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
+ let remote_fs = FakeFs::new(server_cx.executor());
+ remote_fs
+ .insert_tree("/project", serde_json::json!({ ".git":{} }))
+ .await;
+
+ let branches = ["main", "dev", "feature-1"];
+ remote_fs.insert_branches(Path::new("/project/.git"), &branches);
+
+ // User A connects to the remote project via SSH.
+ server_cx.update(HeadlessProject::init);
+ let remote_http_client = Arc::new(BlockedHttpClient);
+ let node = NodeRuntime::unavailable();
+ let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
+ let headless_project = server_cx.new_model(|cx| {
+ client::init_settings(cx);
+ HeadlessProject::new(
+ HeadlessAppState {
+ session: server_ssh,
+ fs: remote_fs.clone(),
+ http_client: remote_http_client,
+ node_runtime: node,
+ languages,
+ },
+ cx,
+ )
+ });
+
+ let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
+ let (project_a, worktree_id) = client_a
+ .build_ssh_project("/project", client_ssh, cx_a)
+ .await;
+
+ // While the SSH worktree is being scanned, user A shares the remote project.
+ let active_call_a = cx_a.read(ActiveCall::global);
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+
+ // User B joins the project.
+ let project_b = client_b.join_remote_project(project_id, cx_b).await;
+
+ // Give client A sometime to see that B has joined, and that the headless server
+ // has some git repositories
+ executor.run_until_parked();
+
+ let root_path = ProjectPath::root_path(worktree_id);
+
+ let branches_b = cx_b
+ .update(|cx| project_b.update(cx, |project, cx| project.branches(root_path.clone(), cx)))
+ .await
+ .unwrap();
+
+ let new_branch = branches[2];
+
+ let branches_b = branches_b
+ .into_iter()
+ .map(|branch| branch.name)
+ .collect::>();
+
+ assert_eq!(&branches_b, &branches);
+
+ cx_b.update(|cx| {
+ project_b.update(cx, |project, cx| {
+ project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
+ })
+ })
+ .await
+ .unwrap();
+
+ executor.run_until_parked();
+
+ let server_branch = server_cx.update(|cx| {
+ headless_project.update(cx, |headless_project, cx| {
+ headless_project
+ .worktree_store
+ .update(cx, |worktree_store, cx| {
+ worktree_store
+ .current_branch(root_path.clone(), cx)
+ .unwrap()
+ })
+ })
+ });
+
+ assert_eq!(server_branch.as_ref(), branches[2]);
+
+ // Also try creating a new branch
+ cx_b.update(|cx| {
+ project_b.update(cx, |project, cx| {
+ project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
+ })
+ })
+ .await
+ .unwrap();
+
+ executor.run_until_parked();
+
+ let server_branch = server_cx.update(|cx| {
+ headless_project.update(cx, |headless_project, cx| {
+ headless_project
+ .worktree_store
+ .update(cx, |worktree_store, cx| {
+ worktree_store.current_branch(root_path, cx).unwrap()
+ })
+ })
+ });
+
+ assert_eq!(server_branch.as_ref(), "totally-new-branch");
+}
+
+#[gpui::test]
+async fn test_ssh_collaboration_formatting_with_prettier(
+ executor: BackgroundExecutor,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+ server_cx: &mut TestAppContext,
+) {
+ cx_a.set_name("a");
+ cx_b.set_name("b");
+ server_cx.set_name("server");
+
+ let mut server = TestServer::start(executor.clone()).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+
+ let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
+ let remote_fs = FakeFs::new(server_cx.executor());
+ let buffer_text = "let one = \"two\"";
+ let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
+ remote_fs
+ .insert_tree("/project", serde_json::json!({ "a.ts": buffer_text }))
+ .await;
+
+ let test_plugin = "test_plugin";
+ let ts_lang = Arc::new(Language::new(
+ LanguageConfig {
+ name: "TypeScript".into(),
+ matcher: LanguageMatcher {
+ path_suffixes: vec!["ts".to_string()],
+ ..LanguageMatcher::default()
+ },
+ ..LanguageConfig::default()
+ },
+ Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
+ ));
+ client_a.language_registry().add(ts_lang.clone());
+ client_b.language_registry().add(ts_lang.clone());
+
+ let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
+ let mut fake_language_servers = languages.register_fake_lsp(
+ "TypeScript",
+ FakeLspAdapter {
+ prettier_plugins: vec![test_plugin],
+ ..Default::default()
+ },
+ );
+
+ // User A connects to the remote project via SSH.
+ server_cx.update(HeadlessProject::init);
+ let remote_http_client = Arc::new(BlockedHttpClient);
+ let _headless_project = server_cx.new_model(|cx| {
+ client::init_settings(cx);
+ HeadlessProject::new(
+ HeadlessAppState {
+ session: server_ssh,
+ fs: remote_fs.clone(),
+ http_client: remote_http_client,
+ node_runtime: NodeRuntime::unavailable(),
+ languages,
+ },
+ cx,
+ )
+ });
+
+ let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
+ let (project_a, worktree_id) = client_a
+ .build_ssh_project("/project", client_ssh, cx_a)
+ .await;
+
+ // While the SSH worktree is being scanned, user A shares the remote project.
+ let active_call_a = cx_a.read(ActiveCall::global);
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+
+ // User B joins the project.
+ let project_b = client_b.join_remote_project(project_id, cx_b).await;
+ executor.run_until_parked();
+
+ // Opens the buffer and formats it
+ let buffer_b = project_b
+ .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx))
+ .await
+ .expect("user B opens buffer for formatting");
+
+ cx_a.update(|cx| {
+ SettingsStore::update_global(cx, |store, cx| {
+ store.update_user_settings::(cx, |file| {
+ file.defaults.formatter = Some(SelectedFormatter::Auto);
+ file.defaults.prettier = Some(PrettierSettings {
+ allowed: true,
+ ..PrettierSettings::default()
+ });
+ });
+ });
+ });
+ cx_b.update(|cx| {
+ SettingsStore::update_global(cx, |store, cx| {
+ store.update_user_settings::(cx, |file| {
+ file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
+ vec![Formatter::LanguageServer { name: None }].into(),
+ )));
+ file.defaults.prettier = Some(PrettierSettings {
+ allowed: true,
+ ..PrettierSettings::default()
+ });
+ });
+ });
+ });
+ let fake_language_server = fake_language_servers.next().await.unwrap();
+ fake_language_server.handle_request::(|_, _| async move {
+ panic!(
+ "Unexpected: prettier should be preferred since it's enabled and language supports it"
+ )
+ });
+
+ project_b
+ .update(cx_b, |project, cx| {
+ project.format(
+ HashSet::from_iter([buffer_b.clone()]),
+ true,
+ FormatTrigger::Save,
+ FormatTarget::Buffer,
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+
+ executor.run_until_parked();
+ assert_eq!(
+ buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
+ buffer_text.to_string() + "\n" + prettier_format_suffix,
+ "Prettier formatting was not applied to client buffer after client's request"
+ );
+
+ // User A opens and formats the same buffer too
+ let buffer_a = project_a
+ .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx))
+ .await
+ .expect("user A opens buffer for formatting");
+
+ cx_a.update(|cx| {
+ SettingsStore::update_global(cx, |store, cx| {
+ store.update_user_settings::(cx, |file| {
+ file.defaults.formatter = Some(SelectedFormatter::Auto);
+ file.defaults.prettier = Some(PrettierSettings {
+ allowed: true,
+ ..PrettierSettings::default()
+ });
+ });
+ });
+ });
+ project_a
+ .update(cx_a, |project, cx| {
+ project.format(
+ HashSet::from_iter([buffer_a.clone()]),
+ true,
+ FormatTrigger::Manual,
+ FormatTarget::Buffer,
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+
+ executor.run_until_parked();
+ assert_eq!(
+ buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
+ buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
+ "Prettier formatting was not applied to client buffer after host's request"
+ );
+}
diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs
index 59f83e06548a6b..14cab63f636deb 100644
--- a/crates/collab_ui/src/collab_panel.rs
+++ b/crates/collab_ui/src/collab_panel.rs
@@ -5,7 +5,7 @@ use self::channel_modal::ChannelModal;
use crate::{channel_view::ChannelView, chat_panel::ChatPanel, CollaborationPanelSettings};
use call::ActiveCall;
use channel::{Channel, ChannelEvent, ChannelStore};
-use client::{ChannelId, Client, Contact, ProjectId, User, UserStore};
+use client::{ChannelId, Client, Contact, User, UserStore};
use contact_finder::ContactFinder;
use db::kvp::KEY_VALUE_STORE;
use editor::{Editor, EditorElement, EditorStyle};
@@ -182,10 +182,6 @@ enum ListEntry {
ChannelEditor {
depth: usize,
},
- HostedProject {
- id: ProjectId,
- name: SharedString,
- },
Contact {
contact: Arc,
calling: bool,
@@ -566,7 +562,6 @@ impl CollabPanel {
}
}
- let hosted_projects = channel_store.projects_for_id(channel.id);
let has_children = channel_store
.channel_at_index(mat.candidate_id + 1)
.map_or(false, |next_channel| {
@@ -600,10 +595,6 @@ impl CollabPanel {
});
}
}
-
- for (name, id) in hosted_projects {
- self.entries.push(ListEntry::HostedProject { id, name });
- }
}
}
@@ -1029,40 +1020,6 @@ impl CollabPanel {
.tooltip(move |cx| Tooltip::text("Open Chat", cx))
}
- fn render_channel_project(
- &self,
- id: ProjectId,
- name: &SharedString,
- is_selected: bool,
- cx: &mut ViewContext,
- ) -> impl IntoElement {
- ListItem::new(ElementId::NamedInteger(
- "channel-project".into(),
- id.0 as usize,
- ))
- .indent_level(2)
- .indent_step_size(px(20.))
- .selected(is_selected)
- .on_click(cx.listener(move |this, _, cx| {
- if let Some(workspace) = this.workspace.upgrade() {
- let app_state = workspace.read(cx).app_state().clone();
- workspace::join_hosted_project(id, app_state, cx).detach_and_prompt_err(
- "Failed to open project",
- cx,
- |_, _| None,
- )
- }
- }))
- .start_slot(
- h_flex()
- .relative()
- .gap_1()
- .child(IconButton::new(0, IconName::FileTree)),
- )
- .child(Label::new(name.clone()))
- .tooltip(move |cx| Tooltip::text("Open Project", cx))
- }
-
fn has_subchannels(&self, ix: usize) -> bool {
self.entries.get(ix).map_or(false, |entry| {
if let ListEntry::Channel { has_children, .. } = entry {
@@ -1538,12 +1495,6 @@ impl CollabPanel {
ListEntry::ChannelChat { channel_id } => {
self.join_channel_chat(*channel_id, cx)
}
- ListEntry::HostedProject {
- id: _id,
- name: _name,
- } => {
- // todo()
- }
ListEntry::OutgoingRequest(_) => {}
ListEntry::ChannelEditor { .. } => {}
}
@@ -2157,10 +2108,6 @@ impl CollabPanel {
ListEntry::ChannelChat { channel_id } => self
.render_channel_chat(*channel_id, is_selected, cx)
.into_any_element(),
-
- ListEntry::HostedProject { id, name } => self
- .render_channel_project(*id, name, is_selected, cx)
- .into_any_element(),
}
}
@@ -2779,7 +2726,7 @@ impl Render for CollabPanel {
.on_action(cx.listener(CollabPanel::collapse_selected_channel))
.on_action(cx.listener(CollabPanel::expand_selected_channel))
.on_action(cx.listener(CollabPanel::start_move_selected_channel))
- .track_focus(&self.focus_handle)
+ .track_focus(&self.focus_handle(cx))
.size_full()
.child(if self.user_store.read(cx).current_user().is_none() {
self.render_signed_out(cx)
@@ -2898,11 +2845,6 @@ impl PartialEq for ListEntry {
return channel_1.id == channel_2.id;
}
}
- ListEntry::HostedProject { id, .. } => {
- if let ListEntry::HostedProject { id: other_id, .. } = other {
- return id == other_id;
- }
- }
ListEntry::ChannelNotes { channel_id } => {
if let ListEntry::ChannelNotes {
channel_id: other_id,
diff --git a/crates/context_servers/src/protocol.rs b/crates/context_servers/src/protocol.rs
index 80a7a7f991a23f..996fc34f462c5f 100644
--- a/crates/context_servers/src/protocol.rs
+++ b/crates/context_servers/src/protocol.rs
@@ -180,6 +180,39 @@ impl InitializedContextServerProtocol {
Ok(completion)
}
+
+ /// List MCP tools.
+ pub async fn list_tools(&self) -> Result {
+ self.check_capability(ServerCapability::Tools)?;
+
+ let response = self
+ .inner
+ .request::(types::RequestType::ListTools.as_str(), ())
+ .await?;
+
+ Ok(response)
+ }
+
+ /// Executes a tool with the given arguments
+ pub async fn run_tool>(
+ &self,
+ tool: P,
+ arguments: Option>,
+ ) -> Result {
+ self.check_capability(ServerCapability::Tools)?;
+
+ let params = types::CallToolParams {
+ name: tool.as_ref().to_string(),
+ arguments,
+ };
+
+ let response: types::CallToolResponse = self
+ .inner
+ .request(types::RequestType::CallTool.as_str(), params)
+ .await?;
+
+ Ok(response)
+ }
}
impl InitializedContextServerProtocol {
diff --git a/crates/context_servers/src/registry.rs b/crates/context_servers/src/registry.rs
index 625f308c15228f..54901870349724 100644
--- a/crates/context_servers/src/registry.rs
+++ b/crates/context_servers/src/registry.rs
@@ -9,7 +9,8 @@ struct GlobalContextServerRegistry(Arc);
impl Global for GlobalContextServerRegistry {}
pub struct ContextServerRegistry {
- registry: RwLock>>>,
+ command_registry: RwLock>>>,
+ tool_registry: RwLock>>>,
}
impl ContextServerRegistry {
@@ -20,13 +21,14 @@ impl ContextServerRegistry {
pub fn register(cx: &mut AppContext) {
cx.set_global(GlobalContextServerRegistry(Arc::new(
ContextServerRegistry {
- registry: RwLock::new(HashMap::default()),
+ command_registry: RwLock::new(HashMap::default()),
+ tool_registry: RwLock::new(HashMap::default()),
},
)))
}
pub fn register_command(&self, server_id: String, command_name: &str) {
- let mut registry = self.registry.write();
+ let mut registry = self.command_registry.write();
registry
.entry(server_id)
.or_default()
@@ -34,14 +36,34 @@ impl ContextServerRegistry {
}
pub fn unregister_command(&self, server_id: &str, command_name: &str) {
- let mut registry = self.registry.write();
+ let mut registry = self.command_registry.write();
if let Some(commands) = registry.get_mut(server_id) {
commands.retain(|name| name.as_ref() != command_name);
}
}
pub fn get_commands(&self, server_id: &str) -> Option>> {
- let registry = self.registry.read();
+ let registry = self.command_registry.read();
+ registry.get(server_id).cloned()
+ }
+
+ pub fn register_tool(&self, server_id: String, tool_name: &str) {
+ let mut registry = self.tool_registry.write();
+ registry
+ .entry(server_id)
+ .or_default()
+ .push(tool_name.into());
+ }
+
+ pub fn unregister_tool(&self, server_id: &str, tool_name: &str) {
+ let mut registry = self.tool_registry.write();
+ if let Some(tools) = registry.get_mut(server_id) {
+ tools.retain(|name| name.as_ref() != tool_name);
+ }
+ }
+
+ pub fn get_tools(&self, server_id: &str) -> Option>> {
+ let registry = self.tool_registry.read();
registry.get(server_id).cloned()
}
}
diff --git a/crates/context_servers/src/types.rs b/crates/context_servers/src/types.rs
index 2bca0a021a1290..b6d8a958bb1264 100644
--- a/crates/context_servers/src/types.rs
+++ b/crates/context_servers/src/types.rs
@@ -16,6 +16,8 @@ pub enum RequestType {
PromptsList,
CompletionComplete,
Ping,
+ ListTools,
+ ListResourceTemplates,
}
impl RequestType {
@@ -32,6 +34,8 @@ impl RequestType {
RequestType::PromptsList => "prompts/list",
RequestType::CompletionComplete => "completion/complete",
RequestType::Ping => "ping",
+ RequestType::ListTools => "tools/list",
+ RequestType::ListResourceTemplates => "resources/templates/list",
}
}
}
@@ -402,3 +406,17 @@ pub struct Completion {
pub values: Vec,
pub total: CompletionTotal,
}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CallToolResponse {
+ pub tool_result: serde_json::Value,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ListToolsResponse {
+ pub tools: Vec,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub next_cursor: Option,
+}
diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs
index da6b969b7222bb..d63710983b5a00 100644
--- a/crates/copilot/src/sign_in.rs
+++ b/crates/copilot/src/sign_in.rs
@@ -185,7 +185,7 @@ impl Render for CopilotCodeVerification {
v_flex()
.id("copilot code verification")
- .track_focus(&self.focus_handle)
+ .track_focus(&self.focus_handle(cx))
.elevation_3(cx)
.w_96()
.items_center()
diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs
index cb6d07e9064610..cef634a41c8241 100644
--- a/crates/diagnostics/src/diagnostics.rs
+++ b/crates/diagnostics/src/diagnostics.rs
@@ -101,7 +101,7 @@ impl Render for ProjectDiagnosticsEditor {
};
div()
- .track_focus(&self.focus_handle)
+ .track_focus(&self.focus_handle(cx))
.when(self.path_states.is_empty(), |el| {
el.key_context("EmptyPane")
})
diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs
index 72a4ac9bcfb01e..2c580c44def3f7 100644
--- a/crates/diagnostics/src/items.rs
+++ b/crates/diagnostics/src/items.rs
@@ -136,11 +136,12 @@ impl DiagnosticIndicator {
}
fn update(&mut self, editor: View, cx: &mut ViewContext) {
- let editor = editor.read(cx);
- let buffer = editor.buffer().read(cx);
- let cursor_position = editor.selections.newest::(cx).head();
+ let (buffer, cursor_position) = editor.update(cx, |editor, cx| {
+ let buffer = editor.buffer().read(cx).snapshot(cx);
+ let cursor_position = editor.selections.newest::(cx).head();
+ (buffer, cursor_position)
+ });
let new_diagnostic = buffer
- .snapshot(cx)
.diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
.filter(|entry| !entry.range.is_empty())
.min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml
index ca9f264789b8e7..3d3a7834f2ac15 100644
--- a/crates/editor/Cargo.toml
+++ b/crates/editor/Cargo.toml
@@ -77,6 +77,7 @@ theme.workspace = true
tree-sitter-html = { workspace = true, optional = true }
tree-sitter-rust = { workspace = true, optional = true }
tree-sitter-typescript = { workspace = true, optional = true }
+unicode-segmentation.workspace = true
unindent = { workspace = true, optional = true }
ui.workspace = true
url.workspace = true
diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs
index af8133fc78a904..18ea4db8384ed6 100644
--- a/crates/editor/src/display_map.rs
+++ b/crates/editor/src/display_map.rs
@@ -21,6 +21,7 @@ mod block_map;
mod crease_map;
mod fold_map;
mod inlay_map;
+pub(crate) mod invisibles;
mod tab_map;
mod wrap_map;
@@ -42,6 +43,7 @@ use gpui::{
pub(crate) use inlay_map::Inlay;
use inlay_map::{InlayMap, InlaySnapshot};
pub use inlay_map::{InlayOffset, InlayPoint};
+use invisibles::{is_invisible, replacement};
use language::{
language_settings::language_settings, ChunkRenderer, OffsetUtf16, Point,
Subscription as BufferSubscription,
@@ -56,6 +58,7 @@ use std::{
any::TypeId,
borrow::Cow,
fmt::Debug,
+ iter,
num::NonZeroU32,
ops::{Add, Range, Sub},
sync::Arc,
@@ -63,7 +66,8 @@ use std::{
use sum_tree::{Bias, TreeMap};
use tab_map::{TabMap, TabSnapshot};
use text::LineIndent;
-use ui::WindowContext;
+use ui::{div, px, IntoElement, ParentElement, SharedString, Styled, WindowContext};
+use unicode_segmentation::UnicodeSegmentation;
use wrap_map::{WrapMap, WrapSnapshot};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -461,6 +465,98 @@ pub struct HighlightedChunk<'a> {
pub renderer: Option,
}
+impl<'a> HighlightedChunk<'a> {
+ fn highlight_invisibles(
+ self,
+ editor_style: &'a EditorStyle,
+ ) -> impl Iterator- + 'a {
+ let mut chars = self.text.chars().peekable();
+ let mut text = self.text;
+ let style = self.style;
+ let is_tab = self.is_tab;
+ let renderer = self.renderer;
+ iter::from_fn(move || {
+ let mut prefix_len = 0;
+ while let Some(&ch) = chars.peek() {
+ if !is_invisible(ch) {
+ prefix_len += ch.len_utf8();
+ chars.next();
+ continue;
+ }
+ if prefix_len > 0 {
+ let (prefix, suffix) = text.split_at(prefix_len);
+ text = suffix;
+ return Some(HighlightedChunk {
+ text: prefix,
+ style,
+ is_tab,
+ renderer: renderer.clone(),
+ });
+ }
+ chars.next();
+ let (prefix, suffix) = text.split_at(ch.len_utf8());
+ text = suffix;
+ if let Some(replacement) = replacement(ch) {
+ let background = editor_style.status.hint_background;
+ let underline = editor_style.status.hint;
+ return Some(HighlightedChunk {
+ text: prefix,
+ style: None,
+ is_tab: false,
+ renderer: Some(ChunkRenderer {
+ render: Arc::new(move |_| {
+ div()
+ .child(replacement)
+ .bg(background)
+ .text_decoration_1()
+ .text_decoration_color(underline)
+ .into_any_element()
+ }),
+ constrain_width: false,
+ }),
+ });
+ } else {
+ let invisible_highlight = HighlightStyle {
+ background_color: Some(editor_style.status.hint_background),
+ underline: Some(UnderlineStyle {
+ color: Some(editor_style.status.hint),
+ thickness: px(1.),
+ wavy: false,
+ }),
+ ..Default::default()
+ };
+ let invisible_style = if let Some(mut style) = style {
+ style.highlight(invisible_highlight);
+ style
+ } else {
+ invisible_highlight
+ };
+
+ return Some(HighlightedChunk {
+ text: prefix,
+ style: Some(invisible_style),
+ is_tab: false,
+ renderer: renderer.clone(),
+ });
+ }
+ }
+
+ if !text.is_empty() {
+ let remainder = text;
+ text = "";
+ Some(HighlightedChunk {
+ text: remainder,
+ style,
+ is_tab,
+ renderer: renderer.clone(),
+ })
+ } else {
+ None
+ }
+ })
+ }
+}
+
#[derive(Clone)]
pub struct DisplaySnapshot {
pub buffer_snapshot: MultiBufferSnapshot,
@@ -686,7 +782,7 @@ impl DisplaySnapshot {
suggestion: Some(editor_style.suggestions_style),
},
)
- .map(|chunk| {
+ .flat_map(|chunk| {
let mut highlight_style = chunk
.syntax_highlight_id
.and_then(|id| id.style(&editor_style.syntax));
@@ -729,6 +825,7 @@ impl DisplaySnapshot {
is_tab: chunk.is_tab,
renderer: chunk.renderer,
}
+ .highlight_invisibles(editor_style)
})
}
@@ -795,12 +892,10 @@ impl DisplaySnapshot {
layout_line.closest_index_for_x(x) as u32
}
- pub fn display_chars_at(
- &self,
- mut point: DisplayPoint,
- ) -> impl Iterator
- + '_ {
+ pub fn grapheme_at(&self, mut point: DisplayPoint) -> Option {
point = DisplayPoint(self.block_snapshot.clip_point(point.0, Bias::Left));
- self.text_chunks(point.row())
+ let chars = self
+ .text_chunks(point.row())
.flat_map(str::chars)
.skip_while({
let mut column = 0;
@@ -810,16 +905,24 @@ impl DisplaySnapshot {
!at_point
}
})
- .map(move |ch| {
- let result = (ch, point);
- if ch == '\n' {
- *point.row_mut() += 1;
- *point.column_mut() = 0;
- } else {
- *point.column_mut() += ch.len_utf8() as u32;
+ .take_while({
+ let mut prev = false;
+ move |char| {
+ let now = char.is_ascii();
+ let end = char.is_ascii() && (char.is_ascii_whitespace() || prev);
+ prev = now;
+ !end
}
- result
- })
+ });
+ chars.collect::().graphemes(true).next().map(|s| {
+ if let Some(invisible) = s.chars().next().filter(|&c| is_invisible(c)) {
+ replacement(invisible).unwrap_or(s).to_owned().into()
+ } else if s == "\n" {
+ " ".into()
+ } else {
+ s.to_owned().into()
+ }
+ })
}
pub fn buffer_chars_at(&self, mut offset: usize) -> impl Iterator
- + '_ {
@@ -1168,16 +1271,21 @@ pub mod tests {
use super::*;
use crate::{movement, test::marked_display_snapshot};
use block_map::BlockPlacement;
- use gpui::{div, font, observe, px, AppContext, BorrowAppContext, Context, Element, Hsla};
+ use gpui::{
+ div, font, observe, px, AppContext, BorrowAppContext, Context, Element, Hsla, Rgba,
+ };
use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
- Buffer, Language, LanguageConfig, LanguageMatcher,
+ Buffer, Diagnostic, DiagnosticEntry, DiagnosticSet, Language, LanguageConfig,
+ LanguageMatcher,
};
+ use lsp::LanguageServerId;
use project::Project;
use rand::{prelude::*, Rng};
use settings::SettingsStore;
use smol::stream::StreamExt;
use std::{env, sync::Arc};
+ use text::PointUtf16;
use theme::{LoadThemes, SyntaxTheme};
use unindent::Unindent as _;
use util::test::{marked_text_ranges, sample_text};
@@ -1832,6 +1940,125 @@ pub mod tests {
);
}
+ #[gpui::test]
+ async fn test_chunks_with_diagnostics_across_blocks(cx: &mut gpui::TestAppContext) {
+ cx.background_executor
+ .set_block_on_ticks(usize::MAX..=usize::MAX);
+
+ let text = r#"
+ struct A {
+ b: usize;
+ }
+ const c: usize = 1;
+ "#
+ .unindent();
+
+ cx.update(|cx| init_test(cx, |_| {}));
+
+ let buffer = cx.new_model(|cx| Buffer::local(text, cx));
+
+ buffer.update(cx, |buffer, cx| {
+ buffer.update_diagnostics(
+ LanguageServerId(0),
+ DiagnosticSet::new(
+ [DiagnosticEntry {
+ range: PointUtf16::new(0, 0)..PointUtf16::new(2, 1),
+ diagnostic: Diagnostic {
+ severity: DiagnosticSeverity::ERROR,
+ group_id: 1,
+ message: "hi".into(),
+ ..Default::default()
+ },
+ }],
+ buffer,
+ ),
+ cx,
+ )
+ });
+
+ let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
+ let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
+
+ let map = cx.new_model(|cx| {
+ DisplayMap::new(
+ buffer,
+ font("Courier"),
+ px(16.0),
+ None,
+ true,
+ 1,
+ 1,
+ 0,
+ FoldPlaceholder::test(),
+ cx,
+ )
+ });
+
+ let black = gpui::black().to_rgb();
+ let red = gpui::red().to_rgb();
+
+ // Insert a block in the middle of a multi-line diagnostic.
+ map.update(cx, |map, cx| {
+ map.highlight_text(
+ TypeId::of::(),
+ vec![
+ buffer_snapshot.anchor_before(Point::new(3, 9))
+ ..buffer_snapshot.anchor_after(Point::new(3, 14)),
+ buffer_snapshot.anchor_before(Point::new(3, 17))
+ ..buffer_snapshot.anchor_after(Point::new(3, 18)),
+ ],
+ red.into(),
+ );
+ map.insert_blocks(
+ [BlockProperties {
+ placement: BlockPlacement::Below(
+ buffer_snapshot.anchor_before(Point::new(1, 0)),
+ ),
+ height: 1,
+ style: BlockStyle::Sticky,
+ render: Box::new(|_| div().into_any()),
+ priority: 0,
+ }],
+ cx,
+ )
+ });
+
+ let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+ let mut chunks = Vec::<(String, Option, Rgba)>::new();
+ for chunk in snapshot.chunks(DisplayRow(0)..DisplayRow(5), true, Default::default()) {
+ let color = chunk
+ .highlight_style
+ .and_then(|style| style.color)
+ .map_or(black, |color| color.to_rgb());
+ if let Some((last_chunk, last_severity, last_color)) = chunks.last_mut() {
+ if *last_severity == chunk.diagnostic_severity && *last_color == color {
+ last_chunk.push_str(chunk.text);
+ continue;
+ }
+ }
+
+ chunks.push((chunk.text.to_string(), chunk.diagnostic_severity, color));
+ }
+
+ assert_eq!(
+ chunks,
+ [
+ (
+ "struct A {\n b: usize;\n".into(),
+ Some(DiagnosticSeverity::ERROR),
+ black
+ ),
+ ("\n".into(), None, black),
+ ("}".into(), Some(DiagnosticSeverity::ERROR), black),
+ ("\nconst c: ".into(), None, black),
+ ("usize".into(), None, red),
+ (" = ".into(), None, black),
+ ("1".into(), None, red),
+ (";\n".into(), None, black),
+ ]
+ );
+ }
+
// todo(linux) fails due to pixel differences in text rendering
#[cfg(target_os = "macos")]
#[gpui::test]
diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs
index d4e39f2df9270e..673b9383bc58f3 100644
--- a/crates/editor/src/display_map/inlay_map.rs
+++ b/crates/editor/src/display_map/inlay_map.rs
@@ -255,6 +255,22 @@ impl<'a> InlayChunks<'a> {
self.buffer_chunk = None;
self.output_offset = new_range.start;
self.max_output_offset = new_range.end;
+
+ let mut highlight_endpoints = Vec::new();
+ if let Some(text_highlights) = self.highlights.text_highlights {
+ if !text_highlights.is_empty() {
+ self.snapshot.apply_text_highlights(
+ &mut self.transforms,
+ &new_range,
+ text_highlights,
+ &mut highlight_endpoints,
+ );
+ self.transforms.seek(&new_range.start, Bias::Right, &());
+ highlight_endpoints.sort();
+ }
+ }
+ self.highlight_endpoints = highlight_endpoints.into_iter().peekable();
+ self.active_highlights.clear();
}
pub fn offset(&self) -> InlayOffset {
diff --git a/crates/editor/src/display_map/invisibles.rs b/crates/editor/src/display_map/invisibles.rs
new file mode 100644
index 00000000000000..794b897603bd66
--- /dev/null
+++ b/crates/editor/src/display_map/invisibles.rs
@@ -0,0 +1,129 @@
+// Invisibility in a Unicode context is not well defined, so we have to guess.
+//
+// We highlight all ASCII control codes, and unicode whitespace because they are likely
+// confused with an ASCII space in a programming context (U+0020).
+//
+// We also highlight the handful of blank non-space characters:
+// U+2800 BRAILLE PATTERN BLANK - Category: So
+// U+115F HANGUL CHOSEONG FILLER - Category: Lo
+// U+1160 HANGUL CHOSEONG FILLER - Category: Lo
+// U+3164 HANGUL FILLER - Category: Lo
+// U+FFA0 HALFWIDTH HANGUL FILLER - Category: Lo
+// U+FFFC OBJECT REPLACEMENT CHARACTER - Category: So
+//
+// For the rest of Unicode, invisibility happens for two reasons:
+// * A Format character (like a byte order mark or right-to-left override)
+// * An invisible Nonspacing Mark character (like U+034F, or variation selectors)
+//
+// We don't consider unassigned codepoints invisible as the font renderer already shows
+// a replacement character in that case (and there are a *lot* of them)
+//
+// Control characters are mostly fine to highlight; except:
+// * U+E0020..=U+E007F are used in emoji flags. We don't highlight them right now, but we could if we tightened our heuristics.
+// * U+200D is used to join characters. We highlight this but don't replace it. As our font system ignores mid-glyph highlights this mostly works to highlight unexpected uses.
+//
+// Nonspacing marks are handled like U+200D. This means that mid-glyph we ignore them, but
+// probably causes issues with end-of-glyph usage.
+//
+// ref: https://invisible-characters.com
+// ref: https://www.compart.com/en/unicode/category/Cf
+// ref: https://gist.github.com/ConradIrwin/f759e1fc29267143c4c7895aa495dca5?h=1
+// ref: https://unicode.org/Public/emoji/13.0/emoji-test.txt
+// https://github.com/bits/UTF-8-Unicode-Test-Documents/blob/master/UTF-8_sequence_separated/utf8_sequence_0-0x10ffff_assigned_including-unprintable-asis.txt
+pub fn is_invisible(c: char) -> bool {
+ if c <= '\u{1f}' {
+ c != '\t' && c != '\n' && c != '\r'
+ } else if c >= '\u{7f}' {
+ c <= '\u{9f}'
+ || (c.is_whitespace() && c != IDEOGRAPHIC_SPACE)
+ || contains(c, &FORMAT)
+ || contains(c, &OTHER)
+ } else {
+ false
+ }
+}
+// ASCII control characters have fancy unicode glyphs, everything else
+// is replaced by a space - unless it is used in combining characters in
+// which case we need to leave it in the string.
+pub(crate) fn replacement(c: char) -> Option<&'static str> {
+ if c <= '\x1f' {
+ Some(C0_SYMBOLS[c as usize])
+ } else if c == '\x7f' {
+ Some(DEL)
+ } else if contains(c, &PRESERVE) {
+ None
+ } else {
+ Some("\u{2007}") // fixed width space
+ }
+}
+// IDEOGRAPHIC SPACE is common alongside Chinese and other wide character sets.
+// We don't highlight this for now (as it already shows up wide in the editor),
+// but could if we tracked state in the classifier.
+const IDEOGRAPHIC_SPACE: char = '\u{3000}';
+
+const C0_SYMBOLS: &'static [&'static str] = &[
+ "␀", "␁", "␂", "␃", "␄", "␅", "␆", "␇", "␈", "␉", "␊", "␋", "␌", "␍", "␎", "␏", "␐", "␑", "␒",
+ "␓", "␔", "␕", "␖", "␗", "␘", "␙", "␚", "␛", "␜", "␝", "␞", "␟",
+];
+const DEL: &'static str = "␡";
+
+// generated using ucd-generate: ucd-generate general-category --include Format --chars ucd-16.0.0
+pub const FORMAT: &'static [(char, char)] = &[
+ ('\u{ad}', '\u{ad}'),
+ ('\u{600}', '\u{605}'),
+ ('\u{61c}', '\u{61c}'),
+ ('\u{6dd}', '\u{6dd}'),
+ ('\u{70f}', '\u{70f}'),
+ ('\u{890}', '\u{891}'),
+ ('\u{8e2}', '\u{8e2}'),
+ ('\u{180e}', '\u{180e}'),
+ ('\u{200b}', '\u{200f}'),
+ ('\u{202a}', '\u{202e}'),
+ ('\u{2060}', '\u{2064}'),
+ ('\u{2066}', '\u{206f}'),
+ ('\u{feff}', '\u{feff}'),
+ ('\u{fff9}', '\u{fffb}'),
+ ('\u{110bd}', '\u{110bd}'),
+ ('\u{110cd}', '\u{110cd}'),
+ ('\u{13430}', '\u{1343f}'),
+ ('\u{1bca0}', '\u{1bca3}'),
+ ('\u{1d173}', '\u{1d17a}'),
+ ('\u{e0001}', '\u{e0001}'),
+ ('\u{e0020}', '\u{e007f}'),
+];
+
+// hand-made base on https://invisible-characters.com (Excluding Cf)
+pub const OTHER: &'static [(char, char)] = &[
+ ('\u{034f}', '\u{034f}'),
+ ('\u{115F}', '\u{1160}'),
+ ('\u{17b4}', '\u{17b5}'),
+ ('\u{180b}', '\u{180d}'),
+ ('\u{2800}', '\u{2800}'),
+ ('\u{3164}', '\u{3164}'),
+ ('\u{fe00}', '\u{fe0d}'),
+ ('\u{ffa0}', '\u{ffa0}'),
+ ('\u{fffc}', '\u{fffc}'),
+ ('\u{e0100}', '\u{e01ef}'),
+];
+
+// a subset of FORMAT/OTHER that may appear within glyphs
+const PRESERVE: &'static [(char, char)] = &[
+ ('\u{034f}', '\u{034f}'),
+ ('\u{200d}', '\u{200d}'),
+ ('\u{17b4}', '\u{17b5}'),
+ ('\u{180b}', '\u{180d}'),
+ ('\u{e0061}', '\u{e007a}'),
+ ('\u{e007f}', '\u{e007f}'),
+];
+
+fn contains(c: char, list: &[(char, char)]) -> bool {
+ for (start, end) in list {
+ if c < *start {
+ return false;
+ }
+ if c <= *end {
+ return true;
+ }
+ }
+ false
+}
diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs
index 95126436488201..1962cf3fbde64c 100644
--- a/crates/editor/src/editor.rs
+++ b/crates/editor/src/editor.rs
@@ -225,7 +225,6 @@ pub fn render_parsed_markdown(
}
}),
);
- // hello
let mut links = Vec::new();
let mut link_ranges = Vec::new();
@@ -3263,9 +3262,21 @@ impl Editor {
}
if enabled && pair.start.ends_with(text.as_ref()) {
- bracket_pair = Some(pair.clone());
- is_bracket_pair_start = true;
- break;
+ let prefix_len = pair.start.len() - text.len();
+ let preceding_text_matches_prefix = prefix_len == 0
+ || (selection.start.column >= (prefix_len as u32)
+ && snapshot.contains_str_at(
+ Point::new(
+ selection.start.row,
+ selection.start.column - (prefix_len as u32),
+ ),
+ &pair.start[..prefix_len],
+ ));
+ if preceding_text_matches_prefix {
+ bracket_pair = Some(pair.clone());
+ is_bracket_pair_start = true;
+ break;
+ }
}
if pair.end.as_str() == text.as_ref() {
bracket_pair = Some(pair.clone());
@@ -3282,8 +3293,6 @@ impl Editor {
self.use_auto_surround && snapshot_settings.use_auto_surround;
if selection.is_empty() {
if is_bracket_pair_start {
- let prefix_len = bracket_pair.start.len() - text.len();
-
// If the inserted text is a suffix of an opening bracket and the
// selection is preceded by the rest of the opening bracket, then
// insert the closing bracket.
@@ -3291,15 +3300,6 @@ impl Editor {
.chars_at(selection.start)
.next()
.map_or(true, |c| scope.should_autoclose_before(c));
- let preceding_text_matches_prefix = prefix_len == 0
- || (selection.start.column >= (prefix_len as u32)
- && snapshot.contains_str_at(
- Point::new(
- selection.start.row,
- selection.start.column - (prefix_len as u32),
- ),
- &bracket_pair.start[..prefix_len],
- ));
let is_closing_quote = if bracket_pair.end == bracket_pair.start
&& bracket_pair.start.len() == 1
@@ -3318,7 +3318,6 @@ impl Editor {
if autoclose
&& bracket_pair.close
&& following_text_allows_autoclose
- && preceding_text_matches_prefix
&& !is_closing_quote
{
let anchor = snapshot.anchor_before(selection.end);
@@ -3803,9 +3802,6 @@ impl Editor {
pub fn newline_below(&mut self, _: &NewlineBelow, cx: &mut ViewContext) {
let buffer = self.buffer.read(cx);
let snapshot = buffer.snapshot(cx);
- //
- //
- //
let mut edits = Vec::new();
let mut rows = Vec::new();
@@ -10006,8 +10002,8 @@ impl Editor {
let Some(provider) = self.semantics_provider.clone() else {
return Task::ready(Ok(Navigated::No));
};
- let buffer = self.buffer.read(cx);
let head = self.selections.newest::(cx).head();
+ let buffer = self.buffer.read(cx);
let (buffer, head) = if let Some(text_anchor) = buffer.text_anchor_for_position(head, cx) {
text_anchor
} else {
@@ -10314,8 +10310,8 @@ impl Editor {
_: &FindAllReferences,
cx: &mut ViewContext,
) -> Option>> {
- let multi_buffer = self.buffer.read(cx);
let selection = self.selections.newest::(cx);
+ let multi_buffer = self.buffer.read(cx);
let head = selection.head();
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
@@ -10722,8 +10718,9 @@ impl Editor {
self.show_local_selections = true;
if moving_cursor {
- let rename_editor = rename.editor.read(cx);
- let cursor_in_rename_editor = rename_editor.selections.newest::(cx).head();
+ let cursor_in_rename_editor = rename.editor.update(cx, |editor, cx| {
+ editor.selections.newest::(cx).head()
+ });
// Update the selection to match the position of the selection inside
// the rename editor.
@@ -10837,7 +10834,7 @@ impl Editor {
fn cancel_language_server_work(
&mut self,
- _: &CancelLanguageServerWork,
+ _: &actions::CancelLanguageServerWork,
cx: &mut ViewContext,
) {
if let Some(project) = self.project.clone() {
@@ -11133,12 +11130,10 @@ impl Editor {
let nested_start_row = foldable_range.0.start.row + 1;
let nested_end_row = foldable_range.0.end.row;
- if current_level == fold_at_level {
- fold_ranges.push(foldable_range);
- }
-
- if current_level <= fold_at_level {
+ if current_level < fold_at_level {
stack.push((nested_start_row, nested_end_row, current_level + 1));
+ } else if current_level == fold_at_level {
+ fold_ranges.push(foldable_range);
}
start_row = nested_end_row + 1;
@@ -11996,9 +11991,9 @@ impl Editor {
}
pub fn copy_file_location(&mut self, _: &CopyFileLocation, cx: &mut ViewContext) {
+ let selection = self.selections.newest::(cx).start.row + 1;
if let Some(file) = self.target_file(cx) {
if let Some(path) = file.path().to_str() {
- let selection = self.selections.newest::(cx).start.row + 1;
cx.write_to_clipboard(ClipboardItem::new_string(format!("{path}:{selection}")));
}
}
@@ -12774,9 +12769,10 @@ impl Editor {
return;
};
+ let selections = self.selections.all::(cx);
let buffer = self.buffer.read(cx);
let mut new_selections_by_buffer = HashMap::default();
- for selection in self.selections.all::(cx) {
+ for selection in selections {
for (buffer, range, _) in
buffer.range_to_buffer_ranges(selection.start..selection.end, cx)
{
@@ -12821,6 +12817,7 @@ impl Editor {
}
fn open_excerpts_common(&mut self, split: bool, cx: &mut ViewContext) {
+ let selections = self.selections.all::(cx);
let buffer = self.buffer.read(cx);
if buffer.is_singleton() {
cx.propagate();
@@ -12833,7 +12830,7 @@ impl Editor {
};
let mut new_selections_by_buffer = HashMap::default();
- for selection in self.selections.all::(cx) {
+ for selection in selections {
for (mut buffer_handle, mut range, _) in
buffer.range_to_buffer_ranges(selection.range(), cx)
{
@@ -12949,7 +12946,7 @@ impl Editor {
fn selection_replacement_ranges(
&self,
range: Range,
- cx: &AppContext,
+ cx: &mut AppContext,
) -> Vec> {
let selections = self.selections.all::(cx);
let newest_selection = selections
@@ -14592,7 +14589,7 @@ pub fn diagnostic_block_renderer(
.relative()
.size_full()
.pl(cx.gutter_dimensions.width)
- .w(cx.max_width + cx.gutter_dimensions.width)
+ .w(cx.max_width - cx.gutter_dimensions.full_width())
.child(
div()
.flex()
diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs
index 99b5cb663789b2..d56b22b4542085 100644
--- a/crates/editor/src/editor_tests.rs
+++ b/crates/editor/src/editor_tests.rs
@@ -1080,6 +1080,112 @@ fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
});
}
+#[gpui::test]
+fn test_fold_at_level(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let view = cx.add_window(|cx| {
+ let buffer = MultiBuffer::build_simple(
+ &"
+ class Foo:
+ # Hello!
+
+ def a():
+ print(1)
+
+ def b():
+ print(2)
+
+
+ class Bar:
+ # World!
+
+ def a():
+ print(1)
+
+ def b():
+ print(2)
+
+
+ "
+ .unindent(),
+ cx,
+ );
+ build_editor(buffer.clone(), cx)
+ });
+
+ _ = view.update(cx, |view, cx| {
+ view.fold_at_level(&FoldAtLevel { level: 2 }, cx);
+ assert_eq!(
+ view.display_text(cx),
+ "
+ class Foo:
+ # Hello!
+
+ def a():⋯
+
+ def b():⋯
+
+
+ class Bar:
+ # World!
+
+ def a():⋯
+
+ def b():⋯
+
+
+ "
+ .unindent(),
+ );
+
+ view.fold_at_level(&FoldAtLevel { level: 1 }, cx);
+ assert_eq!(
+ view.display_text(cx),
+ "
+ class Foo:⋯
+
+
+ class Bar:⋯
+
+
+ "
+ .unindent(),
+ );
+
+ view.unfold_all(&UnfoldAll, cx);
+ view.fold_at_level(&FoldAtLevel { level: 0 }, cx);
+ assert_eq!(
+ view.display_text(cx),
+ "
+ class Foo:
+ # Hello!
+
+ def a():
+ print(1)
+
+ def b():
+ print(2)
+
+
+ class Bar:
+ # World!
+
+ def a():
+ print(1)
+
+ def b():
+ print(2)
+
+
+ "
+ .unindent(),
+ );
+
+ assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text());
+ });
+}
+
#[gpui::test]
fn test_move_cursor(cx: &mut TestAppContext) {
init_test(cx, |_| {});
diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs
index 03c93c92358606..2d87cd4a3a7794 100644
--- a/crates/editor/src/element.rs
+++ b/crates/editor/src/element.rs
@@ -69,6 +69,7 @@ use sum_tree::Bias;
use theme::{ActiveTheme, Appearance, PlayerColor};
use ui::prelude::*;
use ui::{h_flex, ButtonLike, ButtonStyle, ContextMenu, Tooltip};
+use unicode_segmentation::UnicodeSegmentation;
use util::RangeExt;
use util::ResultExt;
use workspace::{item::Item, Workspace};
@@ -836,129 +837,131 @@ impl EditorElement {
let mut selections: Vec<(PlayerColor, Vec)> = Vec::new();
let mut active_rows = BTreeMap::new();
let mut newest_selection_head = None;
- let editor = self.editor.read(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(..) {
- let is_empty = selection.start == selection.end;
- let is_newest = selection == newest;
-
- let layout = SelectionLayout::new(
- selection,
- editor.selections.line_mode,
- editor.cursor_shape,
- &snapshot.display_snapshot,
- is_newest,
- editor.leader_peer_id.is_none(),
- None,
- );
- if is_newest {
- newest_selection_head = Some(layout.head);
- }
+ 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(..) {
+ let is_empty = selection.start == selection.end;
+ let is_newest = selection == newest;
+
+ let layout = SelectionLayout::new(
+ selection,
+ editor.selections.line_mode,
+ editor.cursor_shape,
+ &snapshot.display_snapshot,
+ is_newest,
+ editor.leader_peer_id.is_none(),
+ None,
+ );
+ if is_newest {
+ newest_selection_head = Some(layout.head);
+ }
- for row in cmp::max(layout.active_rows.start.0, start_row.0)
- ..=cmp::min(layout.active_rows.end.0, end_row.0)
- {
- let contains_non_empty_selection =
- active_rows.entry(DisplayRow(row)).or_insert(!is_empty);
- *contains_non_empty_selection |= !is_empty;
+ for row in cmp::max(layout.active_rows.start.0, start_row.0)
+ ..=cmp::min(layout.active_rows.end.0, end_row.0)
+ {
+ let contains_non_empty_selection =
+ active_rows.entry(DisplayRow(row)).or_insert(!is_empty);
+ *contains_non_empty_selection |= !is_empty;
+ }
+ layouts.push(layout);
}
- layouts.push(layout);
- }
- let player = if editor.read_only(cx) {
- cx.theme().players().read_only()
- } else {
- self.style.local_player
- };
+ let player = if editor.read_only(cx) {
+ cx.theme().players().read_only()
+ } else {
+ self.style.local_player
+ };
- selections.push((player, layouts));
- }
+ selections.push((player, layouts));
+ }
- if let Some(collaboration_hub) = &editor.collaboration_hub {
- // When following someone, render the local selections in their color.
- if let Some(leader_id) = editor.leader_peer_id {
- if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) {
- if let Some(participant_index) = collaboration_hub
- .user_participant_indices(cx)
- .get(&collaborator.user_id)
+ if let Some(collaboration_hub) = &editor.collaboration_hub {
+ // When following someone, render the local selections in their color.
+ if let Some(leader_id) = editor.leader_peer_id {
+ if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id)
{
- if let Some((local_selection_style, _)) = selections.first_mut() {
- *local_selection_style = cx
- .theme()
- .players()
- .color_for_participant(participant_index.0);
+ if let Some(participant_index) = collaboration_hub
+ .user_participant_indices(cx)
+ .get(&collaborator.user_id)
+ {
+ if let Some((local_selection_style, _)) = selections.first_mut() {
+ *local_selection_style = cx
+ .theme()
+ .players()
+ .color_for_participant(participant_index.0);
+ }
}
}
}
- }
- let mut remote_selections = HashMap::default();
- for selection in snapshot.remote_selections_in_range(
- &(start_anchor..end_anchor),
- collaboration_hub.as_ref(),
- cx,
- ) {
- let selection_style = Self::get_participant_color(selection.participant_index, cx);
+ let mut remote_selections = HashMap::default();
+ for selection in snapshot.remote_selections_in_range(
+ &(start_anchor..end_anchor),
+ collaboration_hub.as_ref(),
+ cx,
+ ) {
+ let selection_style =
+ Self::get_participant_color(selection.participant_index, cx);
- // Don't re-render the leader's selections, since the local selections
- // match theirs.
- if Some(selection.peer_id) == editor.leader_peer_id {
- continue;
+ // Don't re-render the leader's selections, since the local selections
+ // match theirs.
+ if Some(selection.peer_id) == editor.leader_peer_id {
+ continue;
+ }
+ let key = HoveredCursor {
+ replica_id: selection.replica_id,
+ selection_id: selection.selection.id,
+ };
+
+ let is_shown =
+ editor.show_cursor_names || editor.hovered_cursors.contains_key(&key);
+
+ remote_selections
+ .entry(selection.replica_id)
+ .or_insert((selection_style, Vec::new()))
+ .1
+ .push(SelectionLayout::new(
+ selection.selection,
+ selection.line_mode,
+ selection.cursor_shape,
+ &snapshot.display_snapshot,
+ false,
+ false,
+ if is_shown { selection.user_name } else { None },
+ ));
}
- let key = HoveredCursor {
- replica_id: selection.replica_id,
- selection_id: selection.selection.id,
- };
- let is_shown =
- editor.show_cursor_names || editor.hovered_cursors.contains_key(&key);
-
- remote_selections
- .entry(selection.replica_id)
- .or_insert((selection_style, Vec::new()))
- .1
- .push(SelectionLayout::new(
- selection.selection,
- selection.line_mode,
- selection.cursor_shape,
- &snapshot.display_snapshot,
- false,
- false,
- if is_shown { selection.user_name } else { None },
- ));
+ selections.extend(remote_selections.into_values());
+ } else if !editor.is_focused(cx) && editor.show_cursor_when_unfocused {
+ let player = if editor.read_only(cx) {
+ cx.theme().players().read_only()
+ } else {
+ self.style.local_player
+ };
+ let layouts = snapshot
+ .buffer_snapshot
+ .selections_in_range(&(start_anchor..end_anchor), true)
+ .map(move |(_, line_mode, cursor_shape, selection)| {
+ SelectionLayout::new(
+ selection,
+ line_mode,
+ cursor_shape,
+ &snapshot.display_snapshot,
+ false,
+ false,
+ None,
+ )
+ })
+ .collect::>();
+ selections.push((player, layouts));
}
-
- selections.extend(remote_selections.into_values());
- } else if !editor.is_focused(cx) && editor.show_cursor_when_unfocused {
- let player = if editor.read_only(cx) {
- cx.theme().players().read_only()
- } else {
- self.style.local_player
- };
- let layouts = snapshot
- .buffer_snapshot
- .selections_in_range(&(start_anchor..end_anchor), true)
- .map(move |(_, line_mode, cursor_shape, selection)| {
- SelectionLayout::new(
- selection,
- line_mode,
- cursor_shape,
- &snapshot.display_snapshot,
- false,
- false,
- None,
- )
- })
- .collect::>();
- selections.push((player, layouts));
- }
+ });
(selections, active_rows, newest_selection_head)
}
@@ -1040,24 +1043,17 @@ impl EditorElement {
}
let block_text = if let CursorShape::Block = selection.cursor_shape {
snapshot
- .display_chars_at(cursor_position)
- .next()
+ .grapheme_at(cursor_position)
.or_else(|| {
if cursor_column == 0 {
- snapshot
- .placeholder_text()
- .and_then(|s| s.chars().next())
- .map(|c| (c, cursor_position))
+ snapshot.placeholder_text().and_then(|s| {
+ s.graphemes(true).next().map(|s| s.to_string().into())
+ })
} else {
None
}
})
- .and_then(|(character, _)| {
- let text = if character == '\n' {
- SharedString::from(" ")
- } else {
- SharedString::from(character.to_string())
- };
+ .and_then(|text| {
let len = text.len();
let font = cursor_row_layout
@@ -1939,23 +1935,25 @@ impl EditorElement {
return Vec::new();
}
- let editor = self.editor.read(cx);
- let newest_selection_head = newest_selection_head.unwrap_or_else(|| {
- let newest = editor.selections.newest::(cx);
- SelectionLayout::new(
- newest,
- editor.selections.line_mode,
- editor.cursor_shape,
- &snapshot.display_snapshot,
- true,
- true,
- None,
- )
- .head
+ let (newest_selection_head, is_relative) = self.editor.update(cx, |editor, cx| {
+ let newest_selection_head = newest_selection_head.unwrap_or_else(|| {
+ let newest = editor.selections.newest::(cx);
+ SelectionLayout::new(
+ newest,
+ editor.selections.line_mode,
+ editor.cursor_shape,
+ &snapshot.display_snapshot,
+ true,
+ true,
+ None,
+ )
+ .head
+ });
+ let is_relative = editor.should_use_relative_line_numbers(cx);
+ (newest_selection_head, is_relative)
});
let font_size = self.style.text.font_size.to_pixels(cx.rem_size());
- let is_relative = editor.should_use_relative_line_numbers(cx);
let relative_to = if is_relative {
Some(newest_selection_head.row())
} else {
@@ -4250,7 +4248,16 @@ fn render_inline_blame_entry(
let relative_timestamp = blame_entry_relative_timestamp(&blame_entry);
let author = blame_entry.author.as_deref().unwrap_or_default();
- let text = format!("{}, {}", author, relative_timestamp);
+ let summary_enabled = ProjectSettings::get_global(cx)
+ .git
+ .show_inline_commit_summary();
+
+ let text = match blame_entry.summary.as_ref() {
+ Some(summary) if summary_enabled => {
+ format!("{}, {} - {}", author, relative_timestamp, summary)
+ }
+ _ => format!("{}, {}", author, relative_timestamp),
+ };
let details = blame.read(cx).details_for_entry(&blame_entry);
diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs
index 1ac134530532c0..9dfc379ae70eda 100644
--- a/crates/editor/src/git/blame.rs
+++ b/crates/editor/src/git/blame.rs
@@ -368,12 +368,15 @@ impl GitBlame {
.spawn({
let snapshot = snapshot.clone();
async move {
- let Blame {
+ let Some(Blame {
entries,
permalinks,
messages,
remote_url,
- } = blame.await?;
+ }) = blame.await?
+ else {
+ return Ok(None);
+ };
let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row);
let commit_details = parse_commit_messages(
@@ -385,13 +388,16 @@ impl GitBlame {
)
.await;
- anyhow::Ok((entries, commit_details))
+ anyhow::Ok(Some((entries, commit_details)))
}
})
.await;
this.update(&mut cx, |this, cx| match result {
- Ok((entries, commit_details)) => {
+ Ok(None) => {
+ // Nothing to do, e.g. no repository found
+ }
+ Ok(Some((entries, commit_details))) => {
this.buffer_edits = buffer_edits;
this.buffer_snapshot = snapshot;
this.entries = entries;
@@ -410,11 +416,7 @@ impl GitBlame {
} else {
// If we weren't triggered by a user, we just log errors in the background, instead of sending
// notifications.
- // Except for `NoRepositoryError`, which can happen often if a user has inline-blame turned on
- // and opens a non-git file.
- if error.downcast_ref::().is_none() {
- log::error!("failed to get git blame data: {error:?}");
- }
+ log::error!("failed to get git blame data: {error:?}");
}
}),
})
diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs
index 4a636f673abb7d..31be9e93a94807 100644
--- a/crates/editor/src/hover_links.rs
+++ b/crates/editor/src/hover_links.rs
@@ -706,10 +706,11 @@ pub(crate) async fn find_file(
) -> Option {
project
.update(cx, |project, cx| {
- project.resolve_existing_file_path(&candidate_file_path, buffer, cx)
+ project.resolve_path_in_buffer(&candidate_file_path, buffer, cx)
})
.ok()?
.await
+ .filter(|s| s.is_file())
}
if let Some(existing_path) = check_path(&candidate_file_path, &project, buffer, cx).await {
@@ -1612,4 +1613,46 @@ mod tests {
assert_eq!(file_path.to_str().unwrap(), "/root/dir/file2.rs");
});
}
+
+ #[gpui::test]
+ async fn test_hover_directories(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ // Insert a new file
+ let fs = cx.update_workspace(|workspace, cx| workspace.project().read(cx).fs().clone());
+ fs.as_fake()
+ .insert_file("/root/dir/file2.rs", "This is file2.rs".as_bytes().to_vec())
+ .await;
+
+ cx.set_state(indoc! {"
+ You can't open ../diˇr because it's a directory.
+ "});
+
+ // File does not exist
+ let screen_coord = cx.pixel_position(indoc! {"
+ You can't open ../diˇr because it's a directory.
+ "});
+ cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
+
+ // No highlight
+ cx.update_editor(|editor, cx| {
+ assert!(editor
+ .snapshot(cx)
+ .text_highlight_ranges::()
+ .unwrap_or_default()
+ .1
+ .is_empty());
+ });
+
+ // Does not open the directory
+ cx.simulate_click(screen_coord, Modifiers::secondary_key());
+ cx.update_workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 1));
+ }
}
diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs
index 9200dd7b8c697c..fb198c837c3fa5 100644
--- a/crates/editor/src/hover_popover.rs
+++ b/crates/editor/src/hover_popover.rs
@@ -1,5 +1,5 @@
use crate::{
- display_map::{InlayOffset, ToDisplayPoint},
+ display_map::{invisibles::is_invisible, InlayOffset, ToDisplayPoint},
hover_links::{InlayHighlight, RangeInEditor},
scroll::ScrollAmount,
Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
@@ -11,7 +11,7 @@ use gpui::{
StyleRefinement, Styled, Task, TextStyleRefinement, View, ViewContext,
};
use itertools::Itertools;
-use language::{DiagnosticEntry, Language, LanguageRegistry};
+use language::{Diagnostic, DiagnosticEntry, Language, LanguageRegistry};
use lsp::DiagnosticSeverity;
use markdown::{Markdown, MarkdownStyle};
use multi_buffer::ToOffset;
@@ -259,7 +259,7 @@ fn show_hover(
}
// If there's a diagnostic, assign it on the hover state and notify
- let local_diagnostic = snapshot
+ let mut local_diagnostic = snapshot
.buffer_snapshot
.diagnostics_in_range::<_, usize>(anchor..anchor, false)
// Find the entry with the most specific range
@@ -280,6 +280,41 @@ fn show_hover(
range: entry.range.to_anchors(&snapshot.buffer_snapshot),
})
});
+ if let Some(invisible) = snapshot
+ .buffer_snapshot
+ .chars_at(anchor)
+ .next()
+ .filter(|&c| is_invisible(c))
+ {
+ let after = snapshot.buffer_snapshot.anchor_after(
+ anchor.to_offset(&snapshot.buffer_snapshot) + invisible.len_utf8(),
+ );
+ local_diagnostic = Some(DiagnosticEntry {
+ diagnostic: Diagnostic {
+ severity: DiagnosticSeverity::HINT,
+ message: format!("Unicode character U+{:02X}", invisible as u32),
+ ..Default::default()
+ },
+ range: anchor..after,
+ })
+ } else if let Some(invisible) = snapshot
+ .buffer_snapshot
+ .reversed_chars_at(anchor)
+ .next()
+ .filter(|&c| is_invisible(c))
+ {
+ let before = snapshot.buffer_snapshot.anchor_before(
+ anchor.to_offset(&snapshot.buffer_snapshot) - invisible.len_utf8(),
+ );
+ local_diagnostic = Some(DiagnosticEntry {
+ diagnostic: Diagnostic {
+ severity: DiagnosticSeverity::HINT,
+ message: format!("Unicode character U+{:02X}", invisible as u32),
+ ..Default::default()
+ },
+ range: before..anchor,
+ })
+ }
let diagnostic_popover = if let Some(local_diagnostic) = local_diagnostic {
let text = match local_diagnostic.diagnostic.source {
diff --git a/crates/editor/src/linked_editing_ranges.rs b/crates/editor/src/linked_editing_ranges.rs
index d3e40021737194..853f014ddb4f92 100644
--- a/crates/editor/src/linked_editing_ranges.rs
+++ b/crates/editor/src/linked_editing_ranges.rs
@@ -41,9 +41,9 @@ pub(super) fn refresh_linked_ranges(this: &mut Editor, cx: &mut ViewContext(cx);
let buffer = this.buffer.read(cx);
let mut applicable_selections = vec![];
- let selections = this.selections.all::(cx);
let snapshot = buffer.snapshot(cx);
for selection in selections {
let cursor_position = selection.head();
diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs
index c85e60fdaa92e5..8e1c12b8cd5334 100644
--- a/crates/editor/src/selections_collection.rs
+++ b/crates/editor/src/selections_collection.rs
@@ -8,14 +8,14 @@ use std::{
use collections::HashMap;
use gpui::{AppContext, Model, Pixels};
use itertools::Itertools;
-use language::{Bias, Point, Selection, SelectionGoal, TextDimension, ToPoint};
+use language::{Bias, Point, Selection, SelectionGoal, TextDimension};
use util::post_inc;
use crate::{
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
movement::TextLayoutDetails,
Anchor, DisplayPoint, DisplayRow, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode,
- ToOffset,
+ ToOffset, ToPoint,
};
#[derive(Debug, Clone)]
@@ -96,7 +96,7 @@ impl SelectionsCollection {
pub fn pending>(
&self,
- cx: &AppContext,
+ cx: &mut AppContext,
) -> Option> {
self.pending_anchor()
.as_ref()
@@ -107,7 +107,7 @@ impl SelectionsCollection {
self.pending.as_ref().map(|pending| pending.mode.clone())
}
- pub fn all<'a, D>(&self, cx: &AppContext) -> Vec>
+ pub fn all<'a, D>(&self, cx: &mut AppContext) -> Vec>
where
D: 'a + TextDimension + Ord + Sub,
{
@@ -194,7 +194,7 @@ impl SelectionsCollection {
pub fn disjoint_in_range<'a, D>(
&self,
range: Range,
- cx: &AppContext,
+ cx: &mut AppContext,
) -> Vec>
where
D: 'a + TextDimension + Ord + Sub + std::fmt::Debug,
@@ -239,9 +239,10 @@ impl SelectionsCollection {
pub fn newest>(
&self,
- cx: &AppContext,
+ cx: &mut AppContext,
) -> Selection {
- resolve(self.newest_anchor(), &self.buffer(cx))
+ let buffer = self.buffer(cx);
+ self.newest_anchor().map(|p| p.summary::(&buffer))
}
pub fn newest_display(&self, cx: &mut AppContext) -> Selection {
@@ -262,9 +263,10 @@ impl SelectionsCollection {
pub fn oldest>(
&self,
- cx: &AppContext,
+ cx: &mut AppContext,
) -> Selection {
- resolve(self.oldest_anchor(), &self.buffer(cx))
+ let buffer = self.buffer(cx);
+ self.oldest_anchor().map(|p| p.summary::(&buffer))
}
pub fn first_anchor(&self) -> Selection {
@@ -276,14 +278,14 @@ impl SelectionsCollection {
pub fn first>(
&self,
- cx: &AppContext,
+ cx: &mut AppContext,
) -> Selection {
self.all(cx).first().unwrap().clone()
}
pub fn last>(
&self,
- cx: &AppContext,
+ cx: &mut AppContext,
) -> Selection {
self.all(cx).last().unwrap().clone()
}
@@ -298,7 +300,7 @@ impl SelectionsCollection {
#[cfg(any(test, feature = "test-support"))]
pub fn ranges + std::fmt::Debug>(
&self,
- cx: &AppContext,
+ cx: &mut AppContext,
) -> Vec> {
self.all::(cx)
.iter()
@@ -475,7 +477,7 @@ impl<'a> MutableSelectionsCollection<'a> {
where
T: 'a + ToOffset + ToPoint + TextDimension + Ord + Sub + std::marker::Copy,
{
- let mut selections = self.all(self.cx);
+ let mut selections = self.collection.all(self.cx);
let mut start = range.start.to_offset(&self.buffer());
let mut end = range.end.to_offset(&self.buffer());
let reversed = if start > end {
@@ -649,6 +651,7 @@ impl<'a> MutableSelectionsCollection<'a> {
let mut changed = false;
let display_map = self.display_map();
let selections = self
+ .collection
.all::(self.cx)
.into_iter()
.map(|selection| {
@@ -676,6 +679,7 @@ impl<'a> MutableSelectionsCollection<'a> {
let mut changed = false;
let snapshot = self.buffer().clone();
let selections = self
+ .collection
.all::(self.cx)
.into_iter()
.map(|selection| {
@@ -869,10 +873,3 @@ where
goal: s.goal,
})
}
-
-fn resolve>(
- selection: &Selection,
- buffer: &MultiBufferSnapshot,
-) -> Selection {
- selection.map(|p| p.summary::(buffer))
-}
diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs
index 7234d97c5b77e4..de5065d2656d35 100644
--- a/crates/editor/src/test/editor_test_context.rs
+++ b/crates/editor/src/test/editor_test_context.rs
@@ -17,6 +17,7 @@ use project::{FakeFs, Project};
use std::{
any::TypeId,
ops::{Deref, DerefMut, Range},
+ path::Path,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
@@ -42,17 +43,18 @@ impl EditorTestContext {
pub async fn new(cx: &mut gpui::TestAppContext) -> EditorTestContext {
let fs = FakeFs::new(cx.executor());
// fs.insert_file("/file", "".to_owned()).await;
+ let root = Self::root_path();
fs.insert_tree(
- "/root",
+ root,
serde_json::json!({
"file": "",
}),
)
.await;
- let project = Project::test(fs, ["/root".as_ref()], cx).await;
+ let project = Project::test(fs, [root], cx).await;
let buffer = project
.update(cx, |project, cx| {
- project.open_local_buffer("/root/file", cx)
+ project.open_local_buffer(root.join("file"), cx)
})
.await
.unwrap();
@@ -71,6 +73,16 @@ impl EditorTestContext {
}
}
+ #[cfg(target_os = "windows")]
+ fn root_path() -> &'static Path {
+ Path::new("C:\\root")
+ }
+
+ #[cfg(not(target_os = "windows"))]
+ fn root_path() -> &'static Path {
+ Path::new("/root")
+ }
+
pub async fn for_editor(editor: WindowHandle, cx: &mut gpui::TestAppContext) -> Self {
let editor_view = editor.root_view(cx).unwrap();
Self {
diff --git a/crates/extension/src/extension_lsp_adapter.rs b/crates/extension/src/extension_lsp_adapter.rs
index 25179acec69ed0..1557ef21530148 100644
--- a/crates/extension/src/extension_lsp_adapter.rs
+++ b/crates/extension/src/extension_lsp_adapter.rs
@@ -8,7 +8,8 @@ use collections::HashMap;
use futures::{Future, FutureExt};
use gpui::AsyncAppContext;
use language::{
- CodeLabel, HighlightId, Language, LanguageServerName, LspAdapter, LspAdapterDelegate,
+ CodeLabel, HighlightId, Language, LanguageServerName, LanguageToolchainStore, LspAdapter,
+ LspAdapterDelegate,
};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions};
use serde::Serialize;
@@ -194,6 +195,7 @@ impl LspAdapter for ExtensionLspAdapter {
async fn workspace_configuration(
self: Arc,
delegate: &Arc,
+ _: Arc,
_cx: &mut AsyncAppContext,
) -> Result {
let delegate = delegate.clone();
diff --git a/crates/extension/src/extension_store.rs b/crates/extension/src/extension_store.rs
index 535d68326f9c3e..0a9299a8be8188 100644
--- a/crates/extension/src/extension_store.rs
+++ b/crates/extension/src/extension_store.rs
@@ -37,7 +37,7 @@ use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use indexed_docs::{IndexedDocsRegistry, ProviderId};
use language::{
LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LanguageRegistry,
- QUERY_FILENAME_PREFIXES,
+ LoadedLanguage, QUERY_FILENAME_PREFIXES,
};
use node_runtime::NodeRuntime;
use project::ContextProviderWithTasks;
@@ -1102,14 +1102,21 @@ impl ExtensionStore {
let config = std::fs::read_to_string(language_path.join("config.toml"))?;
let config: LanguageConfig = ::toml::from_str(&config)?;
let queries = load_plugin_queries(&language_path);
- let tasks = std::fs::read_to_string(language_path.join("tasks.json"))
- .ok()
- .and_then(|contents| {
- let definitions = serde_json_lenient::from_str(&contents).log_err()?;
- Some(Arc::new(ContextProviderWithTasks::new(definitions)) as Arc<_>)
- });
-
- Ok((config, queries, tasks))
+ let context_provider =
+ std::fs::read_to_string(language_path.join("tasks.json"))
+ .ok()
+ .and_then(|contents| {
+ let definitions =
+ serde_json_lenient::from_str(&contents).log_err()?;
+ Some(Arc::new(ContextProviderWithTasks::new(definitions)) as Arc<_>)
+ });
+
+ Ok(LoadedLanguage {
+ config,
+ queries,
+ context_provider,
+ toolchain_provider: None,
+ })
},
);
}
diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs
index fb4e192023d914..286acdfc98e6cc 100644
--- a/crates/feature_flags/src/feature_flags.rs
+++ b/crates/feature_flags/src/feature_flags.rs
@@ -59,6 +59,12 @@ impl FeatureFlag for ZedPro {
const NAME: &'static str = "zed-pro";
}
+pub struct NotebookFeatureFlag;
+
+impl FeatureFlag for NotebookFeatureFlag {
+ const NAME: &'static str = "notebooks";
+}
+
pub struct AutoCommand {}
impl FeatureFlag for AutoCommand {
const NAME: &'static str = "auto-command";
diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs
index 299b129d82a90d..ce0e3850576443 100644
--- a/crates/file_finder/src/file_finder.rs
+++ b/crates/file_finder/src/file_finder.rs
@@ -790,9 +790,9 @@ impl FileFinderDelegate {
let mut path_matches = Vec::new();
let abs_file_exists = if let Ok(task) = project.update(&mut cx, |this, cx| {
- this.abs_file_path_exists(query.path_query(), cx)
+ this.resolve_abs_file_path(query.path_query(), cx)
}) {
- task.await
+ task.await.is_some()
} else {
false
};
diff --git a/crates/file_finder/src/new_path_prompt.rs b/crates/file_finder/src/new_path_prompt.rs
index e992dd315fa729..d4492857b4958f 100644
--- a/crates/file_finder/src/new_path_prompt.rs
+++ b/crates/file_finder/src/new_path_prompt.rs
@@ -4,7 +4,7 @@ use gpui::{HighlightStyle, Model, StyledText};
use picker::{Picker, PickerDelegate};
use project::{Entry, PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
use std::{
- path::PathBuf,
+ path::{Path, PathBuf},
sync::{
atomic::{self, AtomicBool},
Arc,
@@ -254,6 +254,7 @@ impl PickerDelegate for NewPathDelegate {
.trim()
.trim_start_matches("./")
.trim_start_matches('/');
+
let (dir, suffix) = if let Some(index) = query.rfind('/') {
let suffix = if index + 1 < query.len() {
Some(query[index + 1..].to_string())
@@ -317,6 +318,14 @@ impl PickerDelegate for NewPathDelegate {
})
}
+ fn confirm_completion(
+ &mut self,
+ _: String,
+ cx: &mut ViewContext>,
+ ) -> Option {
+ self.confirm_update_query(cx)
+ }
+
fn confirm_update_query(&mut self, cx: &mut ViewContext>) -> Option {
let m = self.matches.get(self.selected_index)?;
if m.is_dir(self.project.read(cx), cx) {
@@ -422,7 +431,32 @@ impl NewPathDelegate {
) {
cx.notify();
if query.is_empty() {
- self.matches = vec![];
+ self.matches = self
+ .project
+ .read(cx)
+ .worktrees(cx)
+ .flat_map(|worktree| {
+ let worktree_id = worktree.read(cx).id();
+ worktree
+ .read(cx)
+ .child_entries(Path::new(""))
+ .filter_map(move |entry| {
+ entry.is_dir().then(|| Match {
+ path_match: Some(PathMatch {
+ score: 1.0,
+ positions: Default::default(),
+ worktree_id: worktree_id.to_usize(),
+ path: entry.path.clone(),
+ path_prefix: "".into(),
+ is_dir: entry.is_dir(),
+ distance_to_relative_ancestor: 0,
+ }),
+ suffix: None,
+ })
+ })
+ })
+ .collect();
+
return;
}
diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs
index 0736d4189b7197..be1e91b482fc93 100644
--- a/crates/file_finder/src/open_path_prompt.rs
+++ b/crates/file_finder/src/open_path_prompt.rs
@@ -220,7 +220,11 @@ impl PickerDelegate for OpenPathDelegate {
})
}
- fn confirm_completion(&self, query: String) -> Option {
+ fn confirm_completion(
+ &mut self,
+ query: String,
+ _: &mut ViewContext>,
+ ) -> Option {
Some(
maybe!({
let m = self.matches.get(self.selected_index)?;
diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs
index 5ee2947448c90c..4a84c27dfd09da 100644
--- a/crates/fs/src/fs.rs
+++ b/crates/fs/src/fs.rs
@@ -813,6 +813,7 @@ struct FakeFsState {
root: Arc>,
next_inode: u64,
next_mtime: SystemTime,
+ git_event_tx: smol::channel::Sender,
event_txs: Vec>>,
events_paused: bool,
buffered_events: Vec,
@@ -875,9 +876,11 @@ impl FakeFsState {
canonical_path.clear();
match prefix {
Some(prefix_component) => {
- canonical_path.push(prefix_component.as_os_str());
+ canonical_path = PathBuf::from(prefix_component.as_os_str());
+ // Prefixes like `C:\\` are represented without their trailing slash, so we have to re-add it.
+ canonical_path.push(std::path::MAIN_SEPARATOR_STR);
}
- None => canonical_path.push("/"),
+ None => canonical_path = PathBuf::from(std::path::MAIN_SEPARATOR_STR),
}
}
Component::CurDir => {}
@@ -900,7 +903,7 @@ impl FakeFsState {
}
}
entry_stack.push(entry.clone());
- canonical_path.push(name);
+ canonical_path = canonical_path.join(name);
} else {
return None;
}
@@ -962,9 +965,15 @@ pub static FS_DOT_GIT: std::sync::LazyLock<&'static OsStr> =
#[cfg(any(test, feature = "test-support"))]
impl FakeFs {
+ /// We need to use something large enough for Windows and Unix to consider this a new file.
+ /// https://doc.rust-lang.org/nightly/std/time/struct.SystemTime.html#platform-specific-behavior
+ const SYSTEMTIME_INTERVAL: u64 = 100;
+
pub fn new(executor: gpui::BackgroundExecutor) -> Arc {
- Arc::new(Self {
- executor,
+ let (tx, mut rx) = smol::channel::bounded::(10);
+
+ let this = Arc::new(Self {
+ executor: executor.clone(),
state: Mutex::new(FakeFsState {
root: Arc::new(Mutex::new(FakeFsEntry::Dir {
inode: 0,
@@ -973,6 +982,7 @@ impl FakeFs {
entries: Default::default(),
git_repo_state: None,
})),
+ git_event_tx: tx,
next_mtime: SystemTime::UNIX_EPOCH,
next_inode: 1,
event_txs: Default::default(),
@@ -981,7 +991,22 @@ impl FakeFs {
read_dir_call_count: 0,
metadata_call_count: 0,
}),
- })
+ });
+
+ executor.spawn({
+ let this = this.clone();
+ async move {
+ while let Some(git_event) = rx.next().await {
+ if let Some(mut state) = this.state.try_lock() {
+ state.emit_event([(git_event, None)]);
+ } else {
+ panic!("Failed to lock file system state, this execution would have caused a test hang");
+ }
+ }
+ }
+ }).detach();
+
+ this
}
pub fn set_next_mtime(&self, next_mtime: SystemTime) {
@@ -995,7 +1020,7 @@ impl FakeFs {
let new_mtime = state.next_mtime;
let new_inode = state.next_inode;
state.next_inode += 1;
- state.next_mtime += Duration::from_nanos(1);
+ state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
state
.write_path(path, move |entry| {
match entry {
@@ -1048,7 +1073,7 @@ impl FakeFs {
let inode = state.next_inode;
let mtime = state.next_mtime;
state.next_inode += 1;
- state.next_mtime += Duration::from_nanos(1);
+ state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
let file = Arc::new(Mutex::new(FakeFsEntry::File {
inode,
mtime,
@@ -1175,7 +1200,12 @@ impl FakeFs {
let mut entry = entry.lock();
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
- let repo_state = git_repo_state.get_or_insert_with(Default::default);
+ let repo_state = git_repo_state.get_or_insert_with(|| {
+ Arc::new(Mutex::new(FakeGitRepositoryState::new(
+ dot_git.to_path_buf(),
+ state.git_event_tx.clone(),
+ )))
+ });
let mut repo_state = repo_state.lock();
f(&mut repo_state);
@@ -1190,7 +1220,22 @@ impl FakeFs {
pub fn set_branch_name(&self, dot_git: &Path, branch: Option>) {
self.with_git_state(dot_git, true, |state| {
- state.branch_name = branch.map(Into::into)
+ let branch = branch.map(Into::into);
+ state.branches.extend(branch.clone());
+ state.current_branch_name = branch.map(Into::into)
+ })
+ }
+
+ pub fn insert_branches(&self, dot_git: &Path, branches: &[&str]) {
+ self.with_git_state(dot_git, true, |state| {
+ if let Some(first) = branches.first() {
+ if state.current_branch_name.is_none() {
+ state.current_branch_name = Some(first.to_string())
+ }
+ }
+ state
+ .branches
+ .extend(branches.iter().map(ToString::to_string));
})
}
@@ -1399,7 +1444,7 @@ impl Fs for FakeFs {
let inode = state.next_inode;
let mtime = state.next_mtime;
- state.next_mtime += Duration::from_nanos(1);
+ state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
state.next_inode += 1;
state.write_path(&cur_path, |entry| {
entry.or_insert_with(|| {
@@ -1425,7 +1470,7 @@ impl Fs for FakeFs {
let mut state = self.state.lock();
let inode = state.next_inode;
let mtime = state.next_mtime;
- state.next_mtime += Duration::from_nanos(1);
+ state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
state.next_inode += 1;
let file = Arc::new(Mutex::new(FakeFsEntry::File {
inode,
@@ -1560,7 +1605,7 @@ impl Fs for FakeFs {
let mut state = self.state.lock();
let mtime = state.next_mtime;
let inode = util::post_inc(&mut state.next_inode);
- state.next_mtime += Duration::from_nanos(1);
+ state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
let source_entry = state.read_path(&source)?;
let content = source_entry.lock().file_content(&source)?.clone();
let mut kind = Some(PathEventKind::Created);
@@ -1830,7 +1875,12 @@ impl Fs for FakeFs {
let mut entry = entry.lock();
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
let state = git_repo_state
- .get_or_insert_with(|| Arc::new(Mutex::new(FakeGitRepositoryState::default())))
+ .get_or_insert_with(|| {
+ Arc::new(Mutex::new(FakeGitRepositoryState::new(
+ abs_dot_git.to_path_buf(),
+ state.git_event_tx.clone(),
+ )))
+ })
.clone();
Some(git::repository::FakeGitRepository::open(state))
} else {
diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs
index fb204fba8266ab..cf07b74ac5d8dc 100644
--- a/crates/git/src/git.rs
+++ b/crates/git/src/git.rs
@@ -1,4 +1,10 @@
+pub mod blame;
+pub mod commit;
+pub mod diff;
mod hosting_provider;
+mod remote;
+pub mod repository;
+pub mod status;
use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
@@ -7,15 +13,9 @@ use std::fmt;
use std::str::FromStr;
use std::sync::LazyLock;
-pub use git2 as libgit;
-
pub use crate::hosting_provider::*;
-
-pub mod blame;
-pub mod commit;
-pub mod diff;
-pub mod repository;
-pub mod status;
+pub use crate::remote::*;
+pub use git2 as libgit;
pub static DOT_GIT: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".git"));
pub static COOKIES: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("cookies"));
diff --git a/crates/git/src/hosting_provider.rs b/crates/git/src/hosting_provider.rs
index 988dae377f71f9..4afbcf42a419bd 100644
--- a/crates/git/src/hosting_provider.rs
+++ b/crates/git/src/hosting_provider.rs
@@ -69,7 +69,7 @@ pub trait GitHostingProvider {
/// Returns a formatted range of line numbers to be placed in a permalink URL.
fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String;
- fn parse_remote_url<'a>(&self, url: &'a str) -> Option>;
+ fn parse_remote_url(&self, url: &str) -> Option;
fn extract_pull_request(
&self,
@@ -111,6 +111,12 @@ impl GitHostingProviderRegistry {
cx.global::().0.clone()
}
+ /// Returns the global [`GitHostingProviderRegistry`], if one is set.
+ pub fn try_global(cx: &AppContext) -> Option> {
+ cx.try_global::()
+ .map(|registry| registry.0.clone())
+ }
+
/// Returns the global [`GitHostingProviderRegistry`].
///
/// Inserts a default [`GitHostingProviderRegistry`] if one does not yet exist.
@@ -153,10 +159,10 @@ impl GitHostingProviderRegistry {
}
}
-#[derive(Debug)]
-pub struct ParsedGitRemote<'a> {
- pub owner: &'a str,
- pub repo: &'a str,
+#[derive(Debug, PartialEq)]
+pub struct ParsedGitRemote {
+ pub owner: Arc,
+ pub repo: Arc,
}
pub fn parse_git_remote_url(
diff --git a/crates/git/src/remote.rs b/crates/git/src/remote.rs
new file mode 100644
index 00000000000000..430836fcf3af3c
--- /dev/null
+++ b/crates/git/src/remote.rs
@@ -0,0 +1,85 @@
+use derive_more::Deref;
+use url::Url;
+
+/// The URL to a Git remote.
+#[derive(Debug, PartialEq, Eq, Clone, Deref)]
+pub struct RemoteUrl(Url);
+
+impl std::str::FromStr for RemoteUrl {
+ type Err = url::ParseError;
+
+ fn from_str(input: &str) -> Result {
+ if input.starts_with("git@") {
+ // Rewrite remote URLs like `git@github.com:user/repo.git` to `ssh://git@github.com/user/repo.git`
+ let ssh_url = input.replacen(':', "/", 1).replace("git@", "ssh://git@");
+ Ok(RemoteUrl(Url::parse(&ssh_url)?))
+ } else {
+ Ok(RemoteUrl(Url::parse(input)?))
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use pretty_assertions::assert_eq;
+
+ use super::*;
+
+ #[test]
+ fn test_parsing_valid_remote_urls() {
+ let valid_urls = vec![
+ (
+ "https://github.com/octocat/zed.git",
+ "https",
+ "github.com",
+ "/octocat/zed.git",
+ ),
+ (
+ "git@github.com:octocat/zed.git",
+ "ssh",
+ "github.com",
+ "/octocat/zed.git",
+ ),
+ (
+ "ssh://git@github.com/octocat/zed.git",
+ "ssh",
+ "github.com",
+ "/octocat/zed.git",
+ ),
+ (
+ "file:///path/to/local/zed",
+ "file",
+ "",
+ "/path/to/local/zed",
+ ),
+ ];
+
+ for (input, expected_scheme, expected_host, expected_path) in valid_urls {
+ let parsed = input.parse::().expect("failed to parse URL");
+ let url = parsed.0;
+ assert_eq!(
+ url.scheme(),
+ expected_scheme,
+ "unexpected scheme for {input:?}",
+ );
+ assert_eq!(
+ url.host_str().unwrap_or(""),
+ expected_host,
+ "unexpected host for {input:?}",
+ );
+ assert_eq!(url.path(), expected_path, "unexpected path for {input:?}");
+ }
+ }
+
+ #[test]
+ fn test_parsing_invalid_remote_urls() {
+ let invalid_urls = vec!["not_a_url", "http://"];
+
+ for url in invalid_urls {
+ assert!(
+ url.parse::().is_err(),
+ "expected \"{url}\" to not parse as a Git remote URL",
+ );
+ }
+ }
+}
diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs
index 1b3686f0218c9a..fe65816cc5950b 100644
--- a/crates/git/src/repository.rs
+++ b/crates/git/src/repository.rs
@@ -1,8 +1,9 @@
use crate::GitHostingProviderRegistry;
use crate::{blame::Blame, status::GitStatus};
use anyhow::{Context, Result};
-use collections::HashMap;
+use collections::{HashMap, HashSet};
use git2::BranchType;
+use gpui::SharedString;
use parking_lot::Mutex;
use rope::Rope;
use serde::{Deserialize, Serialize};
@@ -17,7 +18,7 @@ use util::ResultExt;
#[derive(Clone, Debug, Hash, PartialEq)]
pub struct Branch {
pub is_head: bool,
- pub name: Box,
+ pub name: SharedString,
/// Timestamp of most recent commit, normalized to Unix Epoch format.
pub unix_timestamp: Option,
}
@@ -41,6 +42,7 @@ pub trait GitRepository: Send + Sync {
fn branches(&self) -> Result>;
fn change_branch(&self, _: &str) -> Result<()>;
fn create_branch(&self, _: &str) -> Result<()>;
+ fn branch_exits(&self, _: &str) -> Result;
fn blame(&self, path: &Path, content: Rope) -> Result;
}
@@ -132,6 +134,18 @@ impl GitRepository for RealGitRepository {
GitStatus::new(&self.git_binary_path, &working_directory, path_prefixes)
}
+ fn branch_exits(&self, name: &str) -> Result {
+ let repo = self.repository.lock();
+ let branch = repo.find_branch(name, BranchType::Local);
+ match branch {
+ Ok(_) => Ok(true),
+ Err(e) => match e.code() {
+ git2::ErrorCode::NotFound => Ok(false),
+ _ => Err(anyhow::anyhow!(e)),
+ },
+ }
+ }
+
fn branches(&self) -> Result> {
let repo = self.repository.lock();
let local_branches = repo.branches(Some(BranchType::Local))?;
@@ -139,7 +153,11 @@ impl GitRepository for RealGitRepository {
.filter_map(|branch| {
branch.ok().and_then(|(branch, _)| {
let is_head = branch.is_head();
- let name = branch.name().ok().flatten().map(Box::from)?;
+ let name = branch
+ .name()
+ .ok()
+ .flatten()
+ .map(|name| name.to_string().into())?;
let timestamp = branch.get().peel_to_commit().ok()?.time();
let unix_timestamp = timestamp.seconds();
let timezone_offset = timestamp.offset_minutes();
@@ -201,17 +219,20 @@ impl GitRepository for RealGitRepository {
}
}
-#[derive(Debug, Clone, Default)]
+#[derive(Debug, Clone)]
pub struct FakeGitRepository {
state: Arc>,
}
-#[derive(Debug, Clone, Default)]
+#[derive(Debug, Clone)]
pub struct FakeGitRepositoryState {
+ pub path: PathBuf,
+ pub event_emitter: smol::channel::Sender,
pub index_contents: HashMap,
pub blames: HashMap,
pub worktree_statuses: HashMap,
- pub branch_name: Option,
+ pub current_branch_name: Option,
+ pub branches: HashSet,
}
impl FakeGitRepository {
@@ -220,6 +241,20 @@ impl FakeGitRepository {
}
}
+impl FakeGitRepositoryState {
+ pub fn new(path: PathBuf, event_emitter: smol::channel::Sender) -> Self {
+ FakeGitRepositoryState {
+ path,
+ event_emitter,
+ index_contents: Default::default(),
+ blames: Default::default(),
+ worktree_statuses: Default::default(),
+ current_branch_name: Default::default(),
+ branches: Default::default(),
+ }
+ }
+}
+
impl GitRepository for FakeGitRepository {
fn reload_index(&self) {}
@@ -234,7 +269,7 @@ impl GitRepository for FakeGitRepository {
fn branch_name(&self) -> Option {
let state = self.state.lock();
- state.branch_name.clone()
+ state.current_branch_name.clone()
}
fn head_sha(&self) -> Option {
@@ -264,18 +299,41 @@ impl GitRepository for FakeGitRepository {
}
fn branches(&self) -> Result> {
- Ok(vec![])
+ let state = self.state.lock();
+ let current_branch = &state.current_branch_name;
+ Ok(state
+ .branches
+ .iter()
+ .map(|branch_name| Branch {
+ is_head: Some(branch_name) == current_branch.as_ref(),
+ name: branch_name.into(),
+ unix_timestamp: None,
+ })
+ .collect())
+ }
+
+ fn branch_exits(&self, name: &str) -> Result {
+ let state = self.state.lock();
+ Ok(state.branches.contains(name))
}
fn change_branch(&self, name: &str) -> Result<()> {
let mut state = self.state.lock();
- state.branch_name = Some(name.to_owned());
+ state.current_branch_name = Some(name.to_owned());
+ state
+ .event_emitter
+ .try_send(state.path.clone())
+ .expect("Dropped repo change event");
Ok(())
}
fn create_branch(&self, name: &str) -> Result<()> {
let mut state = self.state.lock();
- state.branch_name = Some(name.to_owned());
+ state.branches.insert(name.to_owned());
+ state
+ .event_emitter
+ .try_send(state.path.clone())
+ .expect("Dropped repo change event");
Ok(())
}
diff --git a/crates/git_hosting_providers/Cargo.toml b/crates/git_hosting_providers/Cargo.toml
index b8ad1ed05d1605..be0ca56eef5199 100644
--- a/crates/git_hosting_providers/Cargo.toml
+++ b/crates/git_hosting_providers/Cargo.toml
@@ -22,8 +22,9 @@ regex.workspace = true
serde.workspace = true
serde_json.workspace = true
url.workspace = true
+util.workspace = true
[dev-dependencies]
-unindent.workspace = true
+indoc.workspace = true
serde_json.workspace = true
pretty_assertions.workspace = true
diff --git a/crates/git_hosting_providers/src/git_hosting_providers.rs b/crates/git_hosting_providers/src/git_hosting_providers.rs
index 864faa9b495d18..2689d797f41263 100644
--- a/crates/git_hosting_providers/src/git_hosting_providers.rs
+++ b/crates/git_hosting_providers/src/git_hosting_providers.rs
@@ -2,6 +2,7 @@ mod providers;
use std::sync::Arc;
+use git::repository::GitRepository;
use git::GitHostingProviderRegistry;
use gpui::AppContext;
@@ -10,17 +11,27 @@ pub use crate::providers::*;
/// Initializes the Git hosting providers.
pub fn init(cx: &AppContext) {
let provider_registry = GitHostingProviderRegistry::global(cx);
-
- // The providers are stored in a `BTreeMap`, so insertion order matters.
- // GitHub comes first.
+ provider_registry.register_hosting_provider(Arc::new(Bitbucket));
+ provider_registry.register_hosting_provider(Arc::new(Codeberg));
+ provider_registry.register_hosting_provider(Arc::new(Gitee));
provider_registry.register_hosting_provider(Arc::new(Github));
+ provider_registry.register_hosting_provider(Arc::new(Gitlab::new()));
+ provider_registry.register_hosting_provider(Arc::new(Sourcehut));
+}
- // Then GitLab.
- provider_registry.register_hosting_provider(Arc::new(Gitlab));
+/// Registers additional Git hosting providers.
+///
+/// These require information from the Git repository to construct, so their
+/// registration is deferred until we have a Git repository initialized.
+pub fn register_additional_providers(
+ provider_registry: Arc