Compare commits
48 Commits
vim-syntax
...
v0.177.4-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
decfd9877a | ||
|
|
661e36f736 | ||
|
|
c80d25174f | ||
|
|
420bb84152 | ||
|
|
b8d1c3c866 | ||
|
|
090c38d872 | ||
|
|
8a0fb9100e | ||
|
|
664ccc48c8 | ||
|
|
b575bc9a9d | ||
|
|
da895a6fd8 | ||
|
|
2309721274 | ||
|
|
fa2f982848 | ||
|
|
78b460f701 | ||
|
|
2d5063b5f5 | ||
|
|
a87929c5fd | ||
|
|
535c949a1a | ||
|
|
4b6fcef379 | ||
|
|
dc374713d8 | ||
|
|
c084706377 | ||
|
|
578c9f826b | ||
|
|
f06cee40df | ||
|
|
1f936eccc7 | ||
|
|
1516ee3e46 | ||
|
|
53af68aa82 | ||
|
|
e897f191f6 | ||
|
|
aba10b73d2 | ||
|
|
46190bd087 | ||
|
|
0b360febad | ||
|
|
11d75c42f1 | ||
|
|
b3de2bf740 | ||
|
|
b2f174a622 | ||
|
|
a3b7c1d9e3 | ||
|
|
f4b83d1fba | ||
|
|
5de7f1bcd5 | ||
|
|
375885e6ec | ||
|
|
7ab9ec904e | ||
|
|
94425051a1 | ||
|
|
7bd4a85a29 | ||
|
|
7d1b50ea85 | ||
|
|
96ce87d2dd | ||
|
|
1c6bf1f9b1 | ||
|
|
8d9d14c2b9 | ||
|
|
46944b679f | ||
|
|
b1386bff7b | ||
|
|
02204dee06 | ||
|
|
a1e613805a | ||
|
|
5852f2e0a4 | ||
|
|
3130b46515 |
84
.github/workflows/ci.yml
vendored
84
.github/workflows/ci.yml
vendored
@@ -236,12 +236,24 @@ jobs:
|
||||
if: always()
|
||||
run: rm -rf ./../.cargo
|
||||
|
||||
windows_tests:
|
||||
windows_clippy:
|
||||
timeout-minutes: 60
|
||||
name: (Windows) Run Clippy and tests
|
||||
name: (Windows) Run Clippy
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: hosted-windows-2
|
||||
steps:
|
||||
# Temporarily Collect some metadata about the hardware behind our runners.
|
||||
- name: GHA Runner Info
|
||||
run: |
|
||||
Invoke-RestMethod -Headers @{"Metadata"="true"} -Method GET -Uri "http://169.254.169.254/metadata/instance/compute?api-version=2023-07-01" |
|
||||
ConvertTo-Json -Depth 10 |
|
||||
jq "{ vm_size: .vmSize, location: .location, os_disk_gb: (.storageProfile.osDisk.diskSizeGB | tonumber), rs_disk_gb: (.storageProfile.resourceDisk.size | tonumber / 1024) }"
|
||||
@{
|
||||
Cores = (Get-CimInstance Win32_Processor).NumberOfCores
|
||||
vCPUs = (Get-CimInstance Win32_Processor).NumberOfLogicalProcessors
|
||||
RamGb = [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 2)
|
||||
cpuid = (Get-CimInstance Win32_Processor).Name.Trim()
|
||||
} | ConvertTo-Json
|
||||
# more info here:- https://github.com/rust-lang/cargo/issues/13020
|
||||
- name: Enable longer pathnames for git
|
||||
run: git config --system core.longpaths true
|
||||
@@ -275,6 +287,69 @@ jobs:
|
||||
working-directory: ${{ env.ZED_WORKSPACE }}
|
||||
run: ./script/clippy.ps1
|
||||
|
||||
- name: Check dev drive space
|
||||
working-directory: ${{ env.ZED_WORKSPACE }}
|
||||
# `setup-dev-driver.ps1` creates a 100GB drive, with CI taking up ~45GB of the drive.
|
||||
run: ./script/exit-ci-if-dev-drive-is-full.ps1 95
|
||||
|
||||
# Since the Windows runners are stateful, so we need to remove the config file to prevent potential bug.
|
||||
- name: Clean CI config file
|
||||
if: always()
|
||||
run: |
|
||||
if (Test-Path "${{ env.CARGO_HOME }}/config.toml") {
|
||||
Remove-Item -Path "${{ env.CARGO_HOME }}/config.toml" -Force
|
||||
}
|
||||
|
||||
# Windows CI takes twice as long as our other platforms and fast github hosted runners are expensive.
|
||||
# But we still want to do CI, so let's only run tests on main and come back to this when we're
|
||||
# ready to self host our Windows CI (e.g. during the push for full Windows support)
|
||||
windows_tests:
|
||||
timeout-minutes: 60
|
||||
name: (Windows) Run Tests
|
||||
if: ${{ github.repository_owner == 'zed-industries' && (github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'windows')) }}
|
||||
runs-on: hosted-windows-2
|
||||
steps:
|
||||
# Temporarily Collect some metadata about the hardware behind our runners.
|
||||
- name: GHA Runner Info
|
||||
run: |
|
||||
Invoke-RestMethod -Headers @{"Metadata"="true"} -Method GET -Uri "http://169.254.169.254/metadata/instance/compute?api-version=2023-07-01" |
|
||||
ConvertTo-Json -Depth 10 |
|
||||
jq "{ vm_size: .vmSize, location: .location, os_disk_gb: (.storageProfile.osDisk.diskSizeGB | tonumber), rs_disk_gb: (.storageProfile.resourceDisk.size | tonumber / 1024) }"
|
||||
@{
|
||||
Cores = (Get-CimInstance Win32_Processor).NumberOfCores
|
||||
vCPUs = (Get-CimInstance Win32_Processor).NumberOfLogicalProcessors
|
||||
RamGb = [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 2)
|
||||
cpuid = (Get-CimInstance Win32_Processor).Name.Trim()
|
||||
} | ConvertTo-Json
|
||||
# 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:
|
||||
clean: false
|
||||
|
||||
- name: Create Dev Drive using ReFS
|
||||
run: ./script/setup-dev-driver.ps1
|
||||
|
||||
# actions/checkout does not let us clone into anywhere outside ${{ github.workspace }}, so we have to copy the clone...
|
||||
- name: Copy Git Repo to Dev Drive
|
||||
run: |
|
||||
Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.ZED_WORKSPACE }}" -Recurse
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
workspaces: ${{ env.ZED_WORKSPACE }}
|
||||
cache-provider: "github"
|
||||
|
||||
- name: Configure CI
|
||||
run: |
|
||||
mkdir -p ${{ env.CARGO_HOME }} -ErrorAction Ignore
|
||||
cp ./.cargo/ci-config.toml ${{ env.CARGO_HOME }}/config.toml
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests_windows
|
||||
with:
|
||||
@@ -292,7 +367,10 @@ jobs:
|
||||
# Since the Windows runners are stateful, so we need to remove the config file to prevent potential bug.
|
||||
- name: Clean CI config file
|
||||
if: always()
|
||||
run: Remove-Item -Path "${{ env.CARGO_HOME }}/config.toml" -Force
|
||||
run: |
|
||||
if (Test-Path "${{ env.CARGO_HOME }}/config.toml") {
|
||||
Remove-Item -Path "${{ env.CARGO_HOME }}/config.toml" -Force
|
||||
}
|
||||
|
||||
bundle-mac:
|
||||
timeout-minutes: 120
|
||||
|
||||
155
Cargo.lock
generated
155
Cargo.lock
generated
@@ -358,6 +358,19 @@ dependencies = [
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askpass"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"smol",
|
||||
"tempfile",
|
||||
"util",
|
||||
"which 6.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "assets"
|
||||
version = "0.1.0"
|
||||
@@ -1178,9 +1191,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-config"
|
||||
version = "1.5.17"
|
||||
version = "1.5.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "490aa7465ee685b2ced076bb87ef654a47724a7844e2c7d3af4e749ce5b875dd"
|
||||
checksum = "50236e4d60fe8458de90a71c0922c761e41755adf091b1b03de1cef537179915"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -1271,9 +1284,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-bedrockruntime"
|
||||
version = "1.75.0"
|
||||
version = "1.74.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ddf7475b6f50a1a5be8edb1bcdf6e4ae00feed5b890d14a3f1f0e14d76f5a16"
|
||||
checksum = "6938541d1948a543bca23303fec4cff9c36bf0e63b8fa3ae1b337bcb9d5b81af"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -1295,9 +1308,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-kinesis"
|
||||
version = "1.62.0"
|
||||
version = "1.61.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e31622345afd0c35d33c1cbba73ccf9fb88e09857413d8963dea2c493e00704d"
|
||||
checksum = "89f2163d8704e8fdcd51ec6c2e0441c418471e422ee9690451b17a1c46344e1a"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -1317,9 +1330,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-s3"
|
||||
version = "1.77.0"
|
||||
version = "1.76.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34e87342432a3de0e94e82c99a7cbd9042f99de029ae1f4e368160f9e9929264"
|
||||
checksum = "66e83401ad7287ad15244d557e35502c2a94105ca5b41d656c391f1a4fc04ca2"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -1351,9 +1364,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sso"
|
||||
version = "1.60.0"
|
||||
version = "1.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60186fab60b24376d3e33b9ff0a43485f99efd470e3b75a9160c849741d63d56"
|
||||
checksum = "16ff718c9ee45cc1ebd4774a0e086bb80a6ab752b4902edf1c9f56b86ee1f770"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -1373,9 +1386,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-ssooidc"
|
||||
version = "1.61.0"
|
||||
version = "1.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7033130ce1ee13e6018905b7b976c915963755aef299c1521897679d6cd4f8ef"
|
||||
checksum = "5183e088715cc135d8d396fdd3bc02f018f0da4c511f53cb8d795b6a31c55809"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -1395,9 +1408,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sts"
|
||||
version = "1.61.0"
|
||||
version = "1.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5c1cac7677179d622b4448b0d31bcb359185295dc6fca891920cfb17e2b5156"
|
||||
checksum = "c9f944ef032717596639cea4a2118a3a457268ef51bbb5fde9637e54c465da00"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -1458,9 +1471,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-checksums"
|
||||
version = "0.63.0"
|
||||
version = "0.62.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db2dc8d842d872529355c72632de49ef8c5a2949a4472f10e802f28cf925770c"
|
||||
checksum = "f2f45a1c384d7a393026bc5f5c177105aa9fa68e4749653b985707ac27d77295"
|
||||
dependencies = [
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-types",
|
||||
@@ -1810,7 +1823,7 @@ dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.10.5",
|
||||
"itertools 0.12.1",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
"log",
|
||||
@@ -1833,7 +1846,7 @@ dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.10.5",
|
||||
"itertools 0.12.1",
|
||||
"log",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
@@ -2404,6 +2417,25 @@ dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cbindgen"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fce8dd7fcfcbf3a0a87d8f515194b49d6135acab73e18bd380d1d93bb1a15eb"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"heck 0.4.1",
|
||||
"indexmap",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"syn 2.0.90",
|
||||
"tempfile",
|
||||
"toml 0.8.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cbindgen"
|
||||
version = "0.28.0"
|
||||
@@ -2501,9 +2533,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.40"
|
||||
version = "0.4.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
|
||||
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
@@ -2511,7 +2543,7 @@ dependencies = [
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3508,10 +3540,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crc64fast-nvme"
|
||||
version = "1.2.0"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4955638f00a809894c947f85a024020a20815b65a5eea633798ea7924edab2b3"
|
||||
checksum = "d5e2ee08013e3f228d6d2394116c4549a6df77708442c62d887d83f68ef2ee37"
|
||||
dependencies = [
|
||||
"cbindgen 0.27.0",
|
||||
"crc",
|
||||
]
|
||||
|
||||
@@ -5337,9 +5370,11 @@ name = "git"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"askpass",
|
||||
"async-trait",
|
||||
"collections",
|
||||
"derive_more",
|
||||
"futures 0.3.31",
|
||||
"git2",
|
||||
"gpui",
|
||||
"http_client",
|
||||
@@ -5353,7 +5388,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"smol",
|
||||
"sum_tree",
|
||||
"tempfile",
|
||||
"text",
|
||||
"time",
|
||||
"unindent",
|
||||
@@ -5397,11 +5431,14 @@ name = "git_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"askpass",
|
||||
"buffer_diff",
|
||||
"collections",
|
||||
"component",
|
||||
"ctor",
|
||||
"db",
|
||||
"editor",
|
||||
"env_logger 0.11.6",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
@@ -5563,7 +5600,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"calloop",
|
||||
"calloop-wayland-source",
|
||||
"cbindgen",
|
||||
"cbindgen 0.28.0",
|
||||
"cocoa 0.26.0",
|
||||
"collections",
|
||||
"core-foundation 0.9.4",
|
||||
@@ -7236,9 +7273,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.170"
|
||||
version = "0.2.169"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
|
||||
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
||||
|
||||
[[package]]
|
||||
name = "libdbus-sys"
|
||||
@@ -9765,15 +9802,6 @@ dependencies = [
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pgvector"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0e8871b6d7ca78348c6cd29b911b94851f3429f0cd403130ca17f26c1fb91a6"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.2"
|
||||
@@ -10191,6 +10219,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
"askpass",
|
||||
"async-trait",
|
||||
"buffer_diff",
|
||||
"client",
|
||||
@@ -10413,7 +10442,7 @@ checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
|
||||
dependencies = [
|
||||
"bytes 1.10.0",
|
||||
"heck 0.5.0",
|
||||
"itertools 0.10.5",
|
||||
"itertools 0.12.1",
|
||||
"log",
|
||||
"multimap 0.10.0",
|
||||
"once_cell",
|
||||
@@ -10446,7 +10475,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.10.5",
|
||||
"itertools 0.12.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
@@ -11012,6 +11041,7 @@ name = "remote"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"askpass",
|
||||
"async-trait",
|
||||
"collections",
|
||||
"fs",
|
||||
@@ -11032,7 +11062,6 @@ dependencies = [
|
||||
"tempfile",
|
||||
"thiserror 1.0.69",
|
||||
"util",
|
||||
"which 6.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -11474,9 +11503,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.6.0"
|
||||
version = "8.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b3aba5104622db5c9fc61098de54708feb732e7763d7faa2fa625899f00bf6f"
|
||||
checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
@@ -11485,9 +11514,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "8.6.0"
|
||||
version = "8.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f198c73be048d2c5aa8e12f7960ad08443e56fd39cc26336719fdb4ea0ebaae"
|
||||
checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -11498,9 +11527,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "8.6.0"
|
||||
version = "8.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a2fcdc9f40c8dc2922842ca9add611ad19f332227fc651d015881ad1552bd9a"
|
||||
checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d"
|
||||
dependencies = [
|
||||
"globset",
|
||||
"sha2",
|
||||
@@ -11775,9 +11804,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.22"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
|
||||
checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"indexmap",
|
||||
@@ -11788,9 +11817,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.8.22"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
|
||||
checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -11853,18 +11882,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sea-orm"
|
||||
version = "1.1.6"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13fba7b2c749b2d0a00303d5cb13e6761e39a4172554bdf930852cac4e7aeabd"
|
||||
checksum = "00733e5418e8ae3758cdb988c3654174e716230cc53ee2cb884207cf86a23029"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"bigdecimal",
|
||||
"chrono",
|
||||
"futures-util",
|
||||
"futures 0.3.31",
|
||||
"log",
|
||||
"ouroboros",
|
||||
"pgvector",
|
||||
"rust_decimal",
|
||||
"sea-orm-macros",
|
||||
"sea-query",
|
||||
@@ -11882,9 +11910,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sea-orm-macros"
|
||||
version = "1.1.6"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2568cff8d35d5150b4276cc0dd766192a587f64b6ece60ae3706e0872c4eb209"
|
||||
checksum = "a98408f82fb4875d41ef469a79944a7da29767c7b3e4028e22188a3dd613b10f"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
@@ -14234,9 +14262,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.25.2"
|
||||
version = "0.25.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5168a515fe492af54c5cc8800ff8c840be09fa5168de45838afaecd3e008bce4"
|
||||
checksum = "b9ac5ea5e7f2f1700842ec071401010b9c59bf735295f6e9fa079c3dc035b167"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"regex",
|
||||
@@ -14804,9 +14832,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.15.1"
|
||||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587"
|
||||
checksum = "8c1f41ffb7cf259f1ecc2876861a17e7142e63ead296f671f81f6ae85903e0d6"
|
||||
dependencies = [
|
||||
"getrandom 0.3.1",
|
||||
"serde",
|
||||
@@ -14908,6 +14936,7 @@ dependencies = [
|
||||
"multi_buffer",
|
||||
"nvim-rs",
|
||||
"parking_lot",
|
||||
"project",
|
||||
"project_panel",
|
||||
"regex",
|
||||
"release_channel",
|
||||
@@ -15929,12 +15958,6 @@ dependencies = [
|
||||
"syn 2.0.90",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.2.0"
|
||||
@@ -16770,7 +16793,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.177.0"
|
||||
version = "0.177.4"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
||||
@@ -3,6 +3,7 @@ resolver = "2"
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/anthropic",
|
||||
"crates/askpass",
|
||||
"crates/assets",
|
||||
"crates/assistant",
|
||||
"crates/assistant2",
|
||||
@@ -209,6 +210,7 @@ edition = "2021"
|
||||
activity_indicator = { path = "crates/activity_indicator" }
|
||||
ai = { path = "crates/ai" }
|
||||
anthropic = { path = "crates/anthropic" }
|
||||
askpass = { path = "crates/askpass" }
|
||||
assets = { path = "crates/assets" }
|
||||
assistant = { path = "crates/assistant" }
|
||||
assistant2 = { path = "crates/assistant2" }
|
||||
@@ -538,7 +540,7 @@ tiny_http = "0.8"
|
||||
toml = "0.8"
|
||||
tokio = { version = "1" }
|
||||
tower-http = "0.4.4"
|
||||
tree-sitter = { version = "0.25.2", features = ["wasm"] }
|
||||
tree-sitter = { version = "0.25.3", features = ["wasm"] }
|
||||
tree-sitter-bash = "0.23"
|
||||
tree-sitter-c = "0.23"
|
||||
tree-sitter-cpp = "0.23"
|
||||
|
||||
@@ -370,10 +370,10 @@
|
||||
"ctrl-shift-v": "markdown::OpenPreview",
|
||||
"ctrl-alt-shift-c": "editor::DisplayCursorNames",
|
||||
"ctrl-alt-y": "git::ToggleStaged",
|
||||
"alt-y": ["git::StageAndNext", { "whole_excerpt": false }],
|
||||
"alt-shift-y": ["git::UnstageAndNext", { "whole_excerpt": false }],
|
||||
"alt-.": ["editor::GoToHunk", { "center_cursor": true }],
|
||||
"alt-,": ["editor::GoToPreviousHunk", { "center_cursor": true }]
|
||||
"alt-y": "git::StageAndNext",
|
||||
"alt-shift-y": "git::UnstageAndNext",
|
||||
"alt-.": "editor::GoToHunk",
|
||||
"alt-,": "editor::GoToPreviousHunk"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -564,8 +564,8 @@
|
||||
"shift-enter": "editor::ExpandExcerpts",
|
||||
"ctrl-alt-enter": "editor::OpenExcerptsSplit",
|
||||
"ctrl-shift-e": "pane::RevealInProjectPanel",
|
||||
"ctrl-f8": ["editor::GoToHunk", { "center_cursor": true }],
|
||||
"ctrl-shift-f8": ["editor::GoToPreviousHunk", { "center_cursor": true }],
|
||||
"ctrl-f8": "editor::GoToHunk",
|
||||
"ctrl-shift-f8": "editor::GoToPreviousHunk",
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
"ctrl-:": "editor::ToggleInlayHints"
|
||||
}
|
||||
@@ -739,6 +739,12 @@
|
||||
"ctrl-enter": "git::Commit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AskPass > Editor",
|
||||
"bindings": {
|
||||
"enter": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel > Editor",
|
||||
"bindings": {
|
||||
@@ -749,14 +755,6 @@
|
||||
"alt-up": "git_panel::FocusChanges"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitCommit > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::Newline",
|
||||
"ctrl-enter": "git::Commit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel && not_editing",
|
||||
"bindings": {
|
||||
|
||||
@@ -142,8 +142,8 @@
|
||||
"cmd-;": "editor::ToggleLineNumbers",
|
||||
"cmd-alt-z": "git::Restore",
|
||||
"cmd-alt-y": "git::ToggleStaged",
|
||||
"cmd-y": ["git::StageAndNext", { "whole_excerpt": false }],
|
||||
"cmd-shift-y": ["git::UnstageAndNext", { "whole_excerpt": false }],
|
||||
"cmd-y": "git::StageAndNext",
|
||||
"cmd-shift-y": "git::UnstageAndNext",
|
||||
"cmd-'": "editor::ToggleSelectedDiffHunks",
|
||||
"cmd-\"": "editor::ExpandAllDiffHunks",
|
||||
"cmd-alt-g b": "editor::ToggleGitBlame",
|
||||
@@ -642,8 +642,8 @@
|
||||
"shift-enter": "editor::ExpandExcerpts",
|
||||
"cmd-alt-enter": "editor::OpenExcerptsSplit",
|
||||
"cmd-shift-e": "pane::RevealInProjectPanel",
|
||||
"cmd-f8": ["editor::GoToHunk", { "center_cursor": true }],
|
||||
"cmd-shift-f8": ["editor::GoToPreviousHunk", { "center_cursor": true }],
|
||||
"cmd-f8": "editor::GoToHunk",
|
||||
"cmd-shift-f8": "editor::GoToPreviousHunk",
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
"ctrl-:": "editor::ToggleInlayHints"
|
||||
}
|
||||
@@ -753,6 +753,13 @@
|
||||
"cmd-enter": "git::Commit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AskPass > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel > Editor",
|
||||
"use_key_equivalents": true,
|
||||
@@ -761,7 +768,8 @@
|
||||
"cmd-enter": "git::Commit",
|
||||
"tab": "git_panel::FocusChanges",
|
||||
"shift-tab": "git_panel::FocusChanges",
|
||||
"alt-up": "git_panel::FocusChanges"
|
||||
"alt-up": "git_panel::FocusChanges",
|
||||
"shift-escape": "git::ExpandCommitEditor"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
"ctrl-alt-shift-b": "editor::GoToTypeDefinitionSplit",
|
||||
"f2": "editor::GoToDiagnostic",
|
||||
"shift-f2": "editor::GoToPreviousDiagnostic",
|
||||
"ctrl-alt-shift-down": ["editor::GoToHunk", { "center_cursor": true }],
|
||||
"ctrl-alt-shift-up": ["editor::GoToPreviousHunk", { "center_cursor": true }],
|
||||
"ctrl-alt-shift-down": "editor::GoToHunk",
|
||||
"ctrl-alt-shift-up": "editor::GoToPreviousHunk",
|
||||
"ctrl-alt-z": "git::Restore",
|
||||
"ctrl-home": "editor::MoveToBeginning",
|
||||
"ctrl-end": "editor::MoveToEnd",
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
"ctrl-f12": "editor::GoToDefinitionSplit",
|
||||
"shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-.": ["editor::GoToHunk", { "center_cursor": true }],
|
||||
"ctrl-,": ["editor::GoToPreviousHunk", { "center_cursor": true }],
|
||||
"ctrl-.": "editor::GoToHunk",
|
||||
"ctrl-,": "editor::GoToPreviousHunk",
|
||||
"ctrl-k ctrl-u": "editor::ConvertToUpperCase",
|
||||
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
"cmd-alt-shift-b": "editor::GoToTypeDefinitionSplit",
|
||||
"f2": "editor::GoToDiagnostic",
|
||||
"shift-f2": "editor::GoToPreviousDiagnostic",
|
||||
"ctrl-alt-shift-down": ["editor::GoToHunk", { "center_cursor": true }],
|
||||
"ctrl-alt-shift-up": ["editor::GoToPreviousHunk", { "center_cursor": true }],
|
||||
"ctrl-alt-shift-down": "editor::GoToHunk",
|
||||
"ctrl-alt-shift-up": "editor::GoToPreviousHunk",
|
||||
"cmd-home": "editor::MoveToBeginning",
|
||||
"cmd-end": "editor::MoveToEnd",
|
||||
"cmd-shift-home": "editor::SelectToBeginning",
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
"alt-cmd-down": "editor::GoToDefinition",
|
||||
"ctrl-alt-cmd-down": "editor::GoToDefinitionSplit",
|
||||
"alt-shift-cmd-down": "editor::FindAllReferences",
|
||||
"ctrl-.": ["editor::GoToHunk", { "center_cursor": true }],
|
||||
"ctrl-,": ["editor::GoToPreviousHunk", { "center_cursor": true }],
|
||||
"ctrl-.": "editor::GoToHunk",
|
||||
"ctrl-,": "editor::GoToPreviousHunk",
|
||||
"cmd-k cmd-u": "editor::ConvertToUpperCase",
|
||||
"cmd-k cmd-l": "editor::ConvertToLowerCase",
|
||||
"cmd-shift-j": "editor::JoinLines",
|
||||
|
||||
@@ -238,8 +238,8 @@
|
||||
"] x": "vim::SelectSmallerSyntaxNode",
|
||||
"] d": "editor::GoToDiagnostic",
|
||||
"[ d": "editor::GoToPreviousDiagnostic",
|
||||
"] c": ["editor::GoToHunk", { "center_cursor": true }],
|
||||
"[ c": ["editor::GoToPreviousHunk", { "center_cursor": true }],
|
||||
"] c": "editor::GoToHunk",
|
||||
"[ c": "editor::GoToPreviousHunk",
|
||||
"g c": "vim::PushToggleComments"
|
||||
}
|
||||
},
|
||||
@@ -448,7 +448,10 @@
|
||||
"d": "vim::CurrentLine",
|
||||
"s": "vim::PushDeleteSurrounds",
|
||||
"o": "editor::ToggleSelectedDiffHunks", // "d o"
|
||||
"p": "git::Restore" // "d p"
|
||||
"shift-o": "git::ToggleStaged",
|
||||
"p": "git::Restore", // "d p"
|
||||
"u": "git::StageAndNext", // "d u"
|
||||
"shift-u": "git::UnstageAndNext" // "d shift-u"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -837,7 +837,15 @@
|
||||
//
|
||||
// The minimum column number to show the inline blame information at
|
||||
// "min_column": 0
|
||||
}
|
||||
},
|
||||
// How git hunks are displayed visually in the editor.
|
||||
// This setting can take two values:
|
||||
//
|
||||
// 1. Show unstaged hunks with a transparent background (default):
|
||||
// "hunk_style": "transparent"
|
||||
// 2. Show unstaged hunks with a pattern background:
|
||||
// "hunk_style": "pattern"
|
||||
"hunk_style": "transparent"
|
||||
},
|
||||
// Configuration for how direnv configuration should be loaded. May take 2 values:
|
||||
// 1. Load direnv configuration using `direnv export json` directly.
|
||||
@@ -851,15 +859,7 @@
|
||||
// Any addition to this list will be merged with the default list.
|
||||
// Globs are matched relative to the worktree root,
|
||||
// except when starting with a slash (/) or equivalent in Windows.
|
||||
"disabled_globs": [
|
||||
"**/.env*",
|
||||
"**/*.pem",
|
||||
"**/*.key",
|
||||
"**/*.cert",
|
||||
"**/*.crt",
|
||||
"**/.dev.vars",
|
||||
"**/secrets.yml"
|
||||
],
|
||||
"disabled_globs": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/.dev.vars", "**/secrets.yml"],
|
||||
// When to show edit predictions previews in buffer.
|
||||
// This setting takes two possible values:
|
||||
// 1. Display predictions inline when there are no language server completions available.
|
||||
|
||||
21
crates/askpass/Cargo.toml
Normal file
21
crates/askpass/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "askpass"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/askpass.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
smol.workspace = true
|
||||
tempfile.workspace = true
|
||||
util.workspace = true
|
||||
which.workspace = true
|
||||
1
crates/askpass/LICENSE-APACHE
Symbolic link
1
crates/askpass/LICENSE-APACHE
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-APACHE
|
||||
194
crates/askpass/src/askpass.rs
Normal file
194
crates/askpass/src/askpass.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(unix)]
|
||||
use anyhow::Context as _;
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
#[cfg(unix)]
|
||||
use futures::{io::BufReader, AsyncBufReadExt as _};
|
||||
#[cfg(unix)]
|
||||
use futures::{select_biased, AsyncWriteExt as _, FutureExt as _};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use gpui::{AsyncApp, BackgroundExecutor, Task};
|
||||
#[cfg(unix)]
|
||||
use smol::fs;
|
||||
#[cfg(unix)]
|
||||
use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener};
|
||||
#[cfg(unix)]
|
||||
use util::ResultExt as _;
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum AskPassResult {
|
||||
CancelledByUser,
|
||||
Timedout,
|
||||
}
|
||||
|
||||
pub struct AskPassDelegate {
|
||||
tx: mpsc::UnboundedSender<(String, oneshot::Sender<String>)>,
|
||||
_task: Task<()>,
|
||||
}
|
||||
|
||||
impl AskPassDelegate {
|
||||
pub fn new(
|
||||
cx: &mut AsyncApp,
|
||||
password_prompt: impl Fn(String, oneshot::Sender<String>, &mut AsyncApp) + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
let (tx, mut rx) = mpsc::unbounded::<(String, oneshot::Sender<String>)>();
|
||||
let task = cx.spawn(|mut cx| async move {
|
||||
while let Some((prompt, channel)) = rx.next().await {
|
||||
password_prompt(prompt, channel, &mut cx);
|
||||
}
|
||||
});
|
||||
Self { tx, _task: task }
|
||||
}
|
||||
|
||||
pub async fn ask_password(&mut self, prompt: String) -> anyhow::Result<String> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.tx.send((prompt, tx)).await?;
|
||||
Ok(rx.await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub struct AskPassSession {
|
||||
script_path: PathBuf,
|
||||
_askpass_task: Task<()>,
|
||||
askpass_opened_rx: Option<oneshot::Receiver<()>>,
|
||||
askpass_kill_master_rx: Option<oneshot::Receiver<()>>,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl AskPassSession {
|
||||
/// This will create a new AskPassSession.
|
||||
/// You must retain this session until the master process exits.
|
||||
#[must_use]
|
||||
pub async fn new(
|
||||
executor: &BackgroundExecutor,
|
||||
mut delegate: AskPassDelegate,
|
||||
) -> anyhow::Result<Self> {
|
||||
let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?;
|
||||
let askpass_socket = temp_dir.path().join("askpass.sock");
|
||||
let askpass_script_path = temp_dir.path().join("askpass.sh");
|
||||
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
|
||||
let listener =
|
||||
UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?;
|
||||
|
||||
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
|
||||
let mut kill_tx = Some(askpass_kill_master_tx);
|
||||
|
||||
let askpass_task = executor.spawn(async move {
|
||||
let mut askpass_opened_tx = Some(askpass_opened_tx);
|
||||
|
||||
while let Ok((mut stream, _)) = listener.accept().await {
|
||||
if let Some(askpass_opened_tx) = askpass_opened_tx.take() {
|
||||
askpass_opened_tx.send(()).ok();
|
||||
}
|
||||
let mut buffer = Vec::new();
|
||||
let mut reader = BufReader::new(&mut stream);
|
||||
if reader.read_until(b'\0', &mut buffer).await.is_err() {
|
||||
buffer.clear();
|
||||
}
|
||||
let prompt = String::from_utf8_lossy(&buffer);
|
||||
if let Some(password) = delegate
|
||||
.ask_password(prompt.to_string())
|
||||
.await
|
||||
.context("failed to get askpass password")
|
||||
.log_err()
|
||||
{
|
||||
stream.write_all(password.as_bytes()).await.log_err();
|
||||
} else {
|
||||
if let Some(kill_tx) = kill_tx.take() {
|
||||
kill_tx.send(()).log_err();
|
||||
}
|
||||
// note: we expect the caller to drop this task when it's done.
|
||||
// We need to keep the stream open until the caller is done to avoid
|
||||
// spurious errors from ssh.
|
||||
std::future::pending::<()>().await;
|
||||
drop(stream);
|
||||
}
|
||||
}
|
||||
drop(temp_dir)
|
||||
});
|
||||
|
||||
anyhow::ensure!(
|
||||
which::which("nc").is_ok(),
|
||||
"Cannot find `nc` command (netcat), which is required to connect over SSH."
|
||||
);
|
||||
|
||||
// Create an askpass script that communicates back to this process.
|
||||
let askpass_script = format!(
|
||||
"{shebang}\n{print_args} | {nc} -U {askpass_socket} 2> /dev/null \n",
|
||||
// on macOS `brew install netcat` provides the GNU netcat implementation
|
||||
// which does not support -U.
|
||||
nc = if cfg!(target_os = "macos") {
|
||||
"/usr/bin/nc"
|
||||
} else {
|
||||
"nc"
|
||||
},
|
||||
askpass_socket = askpass_socket.display(),
|
||||
print_args = "printf '%s\\0' \"$@\"",
|
||||
shebang = "#!/bin/sh",
|
||||
);
|
||||
fs::write(&askpass_script_path, askpass_script).await?;
|
||||
fs::set_permissions(&askpass_script_path, std::fs::Permissions::from_mode(0o755)).await?;
|
||||
|
||||
Ok(Self {
|
||||
script_path: askpass_script_path,
|
||||
_askpass_task: askpass_task,
|
||||
askpass_kill_master_rx: Some(askpass_kill_master_rx),
|
||||
askpass_opened_rx: Some(askpass_opened_rx),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn script_path(&self) -> &Path {
|
||||
&self.script_path
|
||||
}
|
||||
|
||||
// This will run the askpass task forever, resolving as many authentication requests as needed.
|
||||
// The caller is responsible for examining the result of their own commands and cancelling this
|
||||
// future when this is no longer needed. Note that this can only be called once, but due to the
|
||||
// drop order this takes an &mut, so you can `drop()` it after you're done with the master process.
|
||||
pub async fn run(&mut self) -> AskPassResult {
|
||||
let connection_timeout = Duration::from_secs(10);
|
||||
let askpass_opened_rx = self.askpass_opened_rx.take().expect("Only call run once");
|
||||
let askpass_kill_master_rx = self
|
||||
.askpass_kill_master_rx
|
||||
.take()
|
||||
.expect("Only call run once");
|
||||
|
||||
select_biased! {
|
||||
_ = askpass_opened_rx.fuse() => {
|
||||
// Note: this await can only resolve after we are dropped.
|
||||
askpass_kill_master_rx.await.ok();
|
||||
return AskPassResult::CancelledByUser
|
||||
}
|
||||
|
||||
_ = futures::FutureExt::fuse(smol::Timer::after(connection_timeout)) => {
|
||||
return AskPassResult::Timedout
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub struct AskPassSession {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
impl AskPassSession {
|
||||
pub async fn new(_: &BackgroundExecutor, _: AskPassDelegate) -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
path: PathBuf::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn script_path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> AskPassResult {
|
||||
futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(10))).await;
|
||||
AskPassResult::Timedout
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ use language_model::{
|
||||
report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelTextStream, Role,
|
||||
};
|
||||
use language_model_selector::{InlineLanguageModelSelector, LanguageModelSelector};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::{CodeAction, ProjectTransaction};
|
||||
@@ -1589,10 +1589,29 @@ impl Render for PromptEditor {
|
||||
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
InlineLanguageModelSelector::new(self.language_model_selector.clone())
|
||||
.render(window, cx),
|
||||
)
|
||||
.child(LanguageModelSelectorPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
IconButton::new("context", IconName::SettingsAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted),
|
||||
move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"Using {}",
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.active_model()
|
||||
.map(|model| model.name().0)
|
||||
.unwrap_or_else(|| "No model selected".into()),
|
||||
),
|
||||
None,
|
||||
"Change Model",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
gpui::Corner::TopRight,
|
||||
))
|
||||
.map(|el| {
|
||||
let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else {
|
||||
return el;
|
||||
|
||||
@@ -19,7 +19,7 @@ use language_model::{
|
||||
report_assistant_event, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, Role,
|
||||
};
|
||||
use language_model_selector::{InlineLanguageModelSelector, LanguageModelSelector};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use prompt_store::PromptBuilder;
|
||||
use settings::{update_settings_file, Settings};
|
||||
use std::{
|
||||
@@ -506,7 +506,7 @@ struct PromptEditor {
|
||||
impl EventEmitter<PromptEditorEvent> for PromptEditor {}
|
||||
|
||||
impl Render for PromptEditor {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let status = &self.codegen.read(cx).status;
|
||||
let buttons = match status {
|
||||
CodegenStatus::Idle => {
|
||||
@@ -641,10 +641,29 @@ impl Render for PromptEditor {
|
||||
.w_12()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
InlineLanguageModelSelector::new(self.language_model_selector.clone())
|
||||
.render(window, cx),
|
||||
)
|
||||
.child(LanguageModelSelectorPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
IconButton::new("change-model", IconName::SettingsAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted),
|
||||
move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"Using {}",
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.active_model()
|
||||
.map(|model| model.name().0)
|
||||
.unwrap_or_else(|| "No model selected".into()),
|
||||
),
|
||||
None,
|
||||
"Change Model",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
gpui::Corner::TopRight,
|
||||
))
|
||||
.children(
|
||||
if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
|
||||
let error_message = SharedString::from(error.to_string());
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
use assistant_settings::AssistantSettings;
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle};
|
||||
use language_model_selector::{AssistantLanguageModelSelector, LanguageModelSelector};
|
||||
use gpui::{Entity, FocusHandle, SharedString};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use language_model_selector::{
|
||||
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
|
||||
};
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
use ui::prelude::*;
|
||||
use ui::{prelude::*, ButtonLike, PopoverMenuHandle, Tooltip};
|
||||
|
||||
pub struct AssistantModelSelector {
|
||||
pub selector: Entity<LanguageModelSelector>,
|
||||
selector: Entity<LanguageModelSelector>,
|
||||
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl AssistantModelSelector {
|
||||
pub(crate) fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
focus_handle: FocusHandle,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -33,14 +38,54 @@ impl AssistantModelSelector {
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
menu_handle,
|
||||
focus_handle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.menu_handle.toggle(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AssistantModelSelector {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
AssistantLanguageModelSelector::new(self.focus_handle.clone(), self.selector.clone())
|
||||
.render(window, cx)
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let model_name = match active_model {
|
||||
Some(model) => model.name().0,
|
||||
_ => SharedString::from("No model selected"),
|
||||
};
|
||||
|
||||
LanguageModelSelectorPopoverMenu::new(
|
||||
self.selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
),
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Change Model",
|
||||
&ToggleModelSelector,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
gpui::Corner::BottomRight,
|
||||
)
|
||||
.with_handle(self.menu_handle.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -609,7 +609,7 @@ impl AssistantPanel {
|
||||
.id("title")
|
||||
.overflow_x_scroll()
|
||||
.px(DynamicSpacing::Base08.rems(cx))
|
||||
.child(Label::new(title).text_ellipsis()),
|
||||
.child(Label::new(title).truncate()),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
|
||||
@@ -20,6 +20,7 @@ use gpui::{
|
||||
EventEmitter, FocusHandle, Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window,
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelRegistry};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use parking_lot::Mutex;
|
||||
use settings::Settings;
|
||||
use std::cmp;
|
||||
@@ -102,11 +103,9 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
.items_start()
|
||||
.cursor(CursorStyle::Arrow)
|
||||
.on_action(cx.listener(Self::toggle_context_picker))
|
||||
.on_action(cx.listener(|this, action, window, cx| {
|
||||
let selector = this.model_selector.read(cx).selector.clone();
|
||||
selector.update(cx, |selector, cx| {
|
||||
selector.toggle_model_selector(action, window, cx);
|
||||
})
|
||||
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
||||
this.model_selector
|
||||
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
|
||||
}))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
@@ -858,6 +857,7 @@ impl PromptEditor<BufferCodegen> {
|
||||
editor
|
||||
});
|
||||
let context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||
|
||||
let context_strip = cx.new(|cx| {
|
||||
ContextStrip::new(
|
||||
@@ -881,7 +881,13 @@ impl PromptEditor<BufferCodegen> {
|
||||
context_strip,
|
||||
context_picker_menu_handle,
|
||||
model_selector: cx.new(|cx| {
|
||||
AssistantModelSelector::new(fs, prompt_editor.focus_handle(cx), window, cx)
|
||||
AssistantModelSelector::new(
|
||||
fs,
|
||||
model_selector_menu_handle,
|
||||
prompt_editor.focus_handle(cx),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
edited_since_done: false,
|
||||
prompt_history,
|
||||
@@ -1006,6 +1012,7 @@ impl PromptEditor<TerminalCodegen> {
|
||||
editor
|
||||
});
|
||||
let context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||
|
||||
let context_strip = cx.new(|cx| {
|
||||
ContextStrip::new(
|
||||
@@ -1029,7 +1036,13 @@ impl PromptEditor<TerminalCodegen> {
|
||||
context_strip,
|
||||
context_picker_menu_handle,
|
||||
model_selector: cx.new(|cx| {
|
||||
AssistantModelSelector::new(fs, prompt_editor.focus_handle(cx), window, cx)
|
||||
AssistantModelSelector::new(
|
||||
fs,
|
||||
model_selector_menu_handle.clone(),
|
||||
prompt_editor.focus_handle(cx),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
edited_since_done: false,
|
||||
prompt_history,
|
||||
|
||||
@@ -8,6 +8,7 @@ use gpui::{
|
||||
TextStyle, WeakEntity,
|
||||
};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use rope::Point;
|
||||
use settings::Settings;
|
||||
use std::time::Duration;
|
||||
@@ -53,6 +54,7 @@ impl MessageEditor {
|
||||
let context_store = cx.new(|_cx| ContextStore::new(workspace.clone()));
|
||||
let context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::auto_height(10, window, cx);
|
||||
@@ -105,8 +107,15 @@ impl MessageEditor {
|
||||
context_picker_menu_handle,
|
||||
inline_context_picker,
|
||||
inline_context_picker_menu_handle,
|
||||
model_selector: cx
|
||||
.new(|cx| AssistantModelSelector::new(fs, editor.focus_handle(cx), window, cx)),
|
||||
model_selector: cx.new(|cx| {
|
||||
AssistantModelSelector::new(
|
||||
fs,
|
||||
model_selector_menu_handle,
|
||||
editor.focus_handle(cx),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
use_tools: false,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
@@ -297,11 +306,9 @@ impl Render for MessageEditor {
|
||||
v_flex()
|
||||
.key_context("MessageEditor")
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(|this, action, window, cx| {
|
||||
let selector = this.model_selector.read(cx).selector.clone();
|
||||
selector.update(cx, |this, cx| {
|
||||
this.toggle_model_selector(action, window, cx);
|
||||
})
|
||||
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
||||
this.model_selector
|
||||
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
|
||||
}))
|
||||
.on_action(cx.listener(Self::toggle_context_picker))
|
||||
.on_action(cx.listener(Self::remove_all_context))
|
||||
|
||||
@@ -260,7 +260,7 @@ impl RenderOnce for PastThread {
|
||||
.start_slot(
|
||||
div()
|
||||
.max_w_4_5()
|
||||
.child(Label::new(summary).size(LabelSize::Small).text_ellipsis()),
|
||||
.child(Label::new(summary).size(LabelSize::Small).truncate()),
|
||||
)
|
||||
.end_slot(
|
||||
h_flex()
|
||||
@@ -356,7 +356,7 @@ impl RenderOnce for PastContext {
|
||||
.start_slot(
|
||||
div()
|
||||
.max_w_4_5()
|
||||
.child(Label::new(summary).size(LabelSize::Small).text_ellipsis()),
|
||||
.child(Label::new(summary).size(LabelSize::Small).truncate()),
|
||||
)
|
||||
.end_slot(
|
||||
h_flex()
|
||||
|
||||
@@ -37,7 +37,9 @@ use language_model::{
|
||||
LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry,
|
||||
Role,
|
||||
};
|
||||
use language_model_selector::{AssistantLanguageModelSelector, LanguageModelSelector};
|
||||
use language_model_selector::{
|
||||
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::Picker;
|
||||
use project::lsp_store::LocalLspAdapterDelegate;
|
||||
@@ -196,6 +198,7 @@ pub struct ContextEditor {
|
||||
// context editor, we keep a reference here.
|
||||
dragged_file_worktrees: Vec<Entity<Worktree>>,
|
||||
language_model_selector: Entity<LanguageModelSelector>,
|
||||
language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
}
|
||||
|
||||
pub const DEFAULT_TAB_TITLE: &str = "New Chat";
|
||||
@@ -249,21 +252,6 @@ impl ContextEditor {
|
||||
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
|
||||
];
|
||||
|
||||
let fs_clone = fs.clone();
|
||||
let language_model_selector = cx.new(|cx| {
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs_clone.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let sections = context.read(cx).slash_command_output_sections().to_vec();
|
||||
let patch_ranges = context.read(cx).patch_ranges().collect::<Vec<_>>();
|
||||
let slash_commands = context.read(cx).slash_commands().clone();
|
||||
@@ -276,7 +264,7 @@ impl ContextEditor {
|
||||
image_blocks: Default::default(),
|
||||
scroll_position: None,
|
||||
remote_id: None,
|
||||
fs,
|
||||
fs: fs.clone(),
|
||||
workspace,
|
||||
project,
|
||||
pending_slash_command_creases: HashMap::default(),
|
||||
@@ -288,7 +276,20 @@ impl ContextEditor {
|
||||
show_accept_terms: false,
|
||||
slash_menu_handle: Default::default(),
|
||||
dragged_file_worktrees: Vec::new(),
|
||||
language_model_selector,
|
||||
language_model_selector: cx.new(|cx| {
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
language_model_selector_menu_handle: PopoverMenuHandle::default(),
|
||||
};
|
||||
this.update_message_headers(cx);
|
||||
this.update_image_blocks(cx);
|
||||
@@ -2388,6 +2389,46 @@ impl ContextEditor {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_language_model_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
let focus_handle = self.editor().focus_handle(cx).clone();
|
||||
let model_name = match active_model {
|
||||
Some(model) => model.name().0,
|
||||
None => SharedString::from("No model selected"),
|
||||
};
|
||||
|
||||
LanguageModelSelectorPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
),
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Change Model",
|
||||
&ToggleModelSelector,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
gpui::Corner::BottomLeft,
|
||||
)
|
||||
.with_handle(self.language_model_selector_menu_handle.clone())
|
||||
}
|
||||
|
||||
fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let last_error = self.last_error.as_ref()?;
|
||||
|
||||
@@ -2832,7 +2873,7 @@ impl Render for ContextEditor {
|
||||
None
|
||||
};
|
||||
|
||||
let language_model_selector = self.language_model_selector.clone();
|
||||
let language_model_selector = self.language_model_selector_menu_handle.clone();
|
||||
v_flex()
|
||||
.key_context("ContextEditor")
|
||||
.capture_action(cx.listener(ContextEditor::cancel))
|
||||
@@ -2845,10 +2886,8 @@ impl Render for ContextEditor {
|
||||
.on_action(cx.listener(ContextEditor::edit))
|
||||
.on_action(cx.listener(ContextEditor::assist))
|
||||
.on_action(cx.listener(ContextEditor::split))
|
||||
.on_action(move |action, window, cx| {
|
||||
language_model_selector.update(cx, |this, cx| {
|
||||
this.toggle_model_selector(action, window, cx);
|
||||
})
|
||||
.on_action(move |_: &ToggleModelSelector, window, cx| {
|
||||
language_model_selector.toggle(window, cx);
|
||||
})
|
||||
.size_full()
|
||||
.children(self.render_notice(cx))
|
||||
@@ -2887,14 +2926,11 @@ impl Render for ContextEditor {
|
||||
.gap_1()
|
||||
.child(self.render_inject_context_menu(cx))
|
||||
.child(ui::Divider::vertical())
|
||||
.child(div().pl_0p5().child({
|
||||
let focus_handle = self.editor().focus_handle(cx).clone();
|
||||
AssistantLanguageModelSelector::new(
|
||||
focus_handle,
|
||||
self.language_model_selector.clone(),
|
||||
)
|
||||
.render(window, cx)
|
||||
})),
|
||||
.child(
|
||||
div()
|
||||
.pl_0p5()
|
||||
.child(self.render_language_model_selector(cx)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
|
||||
@@ -243,7 +243,7 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
Label::new(info.description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.text_ellipsis(),
|
||||
.truncate(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -56,8 +56,8 @@ pub enum DiffHunkSecondaryStatus {
|
||||
/// A diff hunk resolved to rows in the buffer.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DiffHunk {
|
||||
/// The buffer range, expressed in terms of rows.
|
||||
pub row_range: Range<u32>,
|
||||
/// The buffer range as points.
|
||||
pub range: Range<Point>,
|
||||
/// The range in the buffer to which this hunk corresponds.
|
||||
pub buffer_range: Range<Anchor>,
|
||||
/// The range in the buffer's diff base text to which this hunk corresponds.
|
||||
@@ -362,6 +362,7 @@ impl BufferDiffInner {
|
||||
pending_hunks = secondary.pending_hunks.clone();
|
||||
}
|
||||
|
||||
let max_point = buffer.max_point();
|
||||
let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
|
||||
iter::from_fn(move || loop {
|
||||
let (start_point, (start_anchor, start_base)) = summaries.next()?;
|
||||
@@ -371,7 +372,7 @@ impl BufferDiffInner {
|
||||
continue;
|
||||
}
|
||||
|
||||
if end_point.column > 0 {
|
||||
if end_point.column > 0 && end_point < max_point {
|
||||
end_point.row += 1;
|
||||
end_point.column = 0;
|
||||
end_anchor = buffer.anchor_before(end_point);
|
||||
@@ -416,7 +417,7 @@ impl BufferDiffInner {
|
||||
}
|
||||
|
||||
return Some(DiffHunk {
|
||||
row_range: start_point.row..end_point.row,
|
||||
range: start_point..end_point,
|
||||
diff_base_byte_range: start_base..end_base,
|
||||
buffer_range: start_anchor..end_anchor,
|
||||
secondary_status,
|
||||
@@ -442,14 +443,9 @@ impl BufferDiffInner {
|
||||
|
||||
let hunk = cursor.item()?;
|
||||
let range = hunk.buffer_range.to_point(buffer);
|
||||
let end_row = if range.end.column > 0 {
|
||||
range.end.row + 1
|
||||
} else {
|
||||
range.end.row
|
||||
};
|
||||
|
||||
Some(DiffHunk {
|
||||
row_range: range.start.row..end_row,
|
||||
range,
|
||||
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
|
||||
buffer_range: hunk.buffer_range.clone(),
|
||||
// The secondary status is not used by callers of this method.
|
||||
@@ -667,11 +663,13 @@ impl std::fmt::Debug for BufferDiff {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum BufferDiffEvent {
|
||||
DiffChanged {
|
||||
changed_range: Option<Range<text::Anchor>>,
|
||||
},
|
||||
LanguageChanged,
|
||||
HunksStagedOrUnstaged(Option<Rope>),
|
||||
}
|
||||
|
||||
impl EventEmitter<BufferDiffEvent> for BufferDiff {}
|
||||
@@ -766,6 +764,17 @@ impl BufferDiff {
|
||||
self.secondary_diff.clone()
|
||||
}
|
||||
|
||||
pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some(secondary_diff) = &self.secondary_diff {
|
||||
secondary_diff.update(cx, |diff, _| {
|
||||
diff.inner.pending_hunks.clear();
|
||||
});
|
||||
cx.emit(BufferDiffEvent::DiffChanged {
|
||||
changed_range: Some(Anchor::MIN..Anchor::MAX),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stage_or_unstage_hunks(
|
||||
&mut self,
|
||||
stage: bool,
|
||||
@@ -788,6 +797,9 @@ impl BufferDiff {
|
||||
}
|
||||
});
|
||||
}
|
||||
cx.emit(BufferDiffEvent::HunksStagedOrUnstaged(
|
||||
new_index_text.clone(),
|
||||
));
|
||||
if let Some((first, last)) = hunks.first().zip(hunks.last()) {
|
||||
let changed_range = first.buffer_range.start..last.buffer_range.end;
|
||||
cx.emit(BufferDiffEvent::DiffChanged {
|
||||
@@ -904,6 +916,14 @@ impl BufferDiff {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hunks<'a>(
|
||||
&'a self,
|
||||
buffer_snapshot: &'a text::BufferSnapshot,
|
||||
cx: &'a App,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
self.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer_snapshot, cx)
|
||||
}
|
||||
|
||||
pub fn hunks_intersecting_range<'a>(
|
||||
&'a self,
|
||||
range: Range<text::Anchor>,
|
||||
@@ -1136,12 +1156,10 @@ pub fn assert_hunks<Iter>(
|
||||
let actual_hunks = diff_hunks
|
||||
.map(|hunk| {
|
||||
(
|
||||
hunk.row_range.clone(),
|
||||
hunk.range.clone(),
|
||||
&diff_base[hunk.diff_base_byte_range.clone()],
|
||||
buffer
|
||||
.text_for_range(
|
||||
Point::new(hunk.row_range.start, 0)..Point::new(hunk.row_range.end, 0),
|
||||
)
|
||||
.text_for_range(hunk.range.clone())
|
||||
.collect::<String>(),
|
||||
hunk.status(),
|
||||
)
|
||||
@@ -1150,7 +1168,14 @@ pub fn assert_hunks<Iter>(
|
||||
|
||||
let expected_hunks: Vec<_> = expected_hunks
|
||||
.iter()
|
||||
.map(|(r, s, h, status)| (r.clone(), *s, h.to_string(), *status))
|
||||
.map(|(r, old_text, new_text, status)| {
|
||||
(
|
||||
Point::new(r.start, 0)..Point::new(r.end, 0),
|
||||
*old_text,
|
||||
new_text.to_string(),
|
||||
*status,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(actual_hunks, expected_hunks);
|
||||
|
||||
@@ -308,7 +308,7 @@ impl Server {
|
||||
.add_request_handler(forward_read_only_project_request::<proto::InlayHints>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::ResolveInlayHint>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitBranches>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitGetBranches>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenUnstagedDiff>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenUncommittedDiff>)
|
||||
.add_request_handler(
|
||||
@@ -393,9 +393,6 @@ impl Server {
|
||||
.add_request_handler(forward_mutating_project_request::<proto::OpenContext>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::CreateContext>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::SynchronizeContexts>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Push>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Pull>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Fetch>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Stage>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Unstage>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Commit>)
|
||||
@@ -405,6 +402,9 @@ impl Server {
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitCheckoutFiles>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::SetIndexText>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::OpenCommitMessageBuffer>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GitCreateBranch>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GitChangeBranch>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::CheckForPushedCommits>)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
|
||||
.add_message_handler(update_context)
|
||||
.add_request_handler({
|
||||
|
||||
@@ -2027,6 +2027,15 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
let buffer_id_b = editor_b.update(cx_b, |editor_b, cx| {
|
||||
editor_b
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.remote_id()
|
||||
});
|
||||
|
||||
// client_b now requests git blame for the open buffer
|
||||
editor_b.update_in(cx_b, |editor_b, window, cx| {
|
||||
@@ -2045,6 +2054,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
&(0..4)
|
||||
.map(|row| RowInfo {
|
||||
buffer_row: Some(row),
|
||||
buffer_id: Some(buffer_id_b),
|
||||
..Default::default()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -2092,6 +2102,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
&(0..4)
|
||||
.map(|row| RowInfo {
|
||||
buffer_row: Some(row),
|
||||
buffer_id: Some(buffer_id_b),
|
||||
..Default::default()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -2127,6 +2138,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
&(0..4)
|
||||
.map(|row| RowInfo {
|
||||
buffer_row: Some(row),
|
||||
buffer_id: Some(buffer_id_b),
|
||||
..Default::default()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
|
||||
@@ -6741,19 +6741,24 @@ async fn test_remote_git_branches(
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
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.
|
||||
// Client A sees that a guest has joined and the repo has been populated
|
||||
executor.run_until_parked();
|
||||
|
||||
let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
|
||||
|
||||
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)))
|
||||
.update(|cx| repo_b.update(cx, |repository, _| repository.branches()))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let new_branch = branches[2];
|
||||
@@ -6765,13 +6770,10 @@ async fn test_remote_git_branches(
|
||||
|
||||
assert_eq!(branches_b, branches_set);
|
||||
|
||||
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();
|
||||
cx_b.update(|cx| repo_b.read(cx).change_branch(new_branch.to_string()))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
@@ -6789,11 +6791,21 @@ async fn test_remote_git_branches(
|
||||
|
||||
// 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)
|
||||
})
|
||||
repo_b
|
||||
.read(cx)
|
||||
.create_branch("totally-new-branch".to_string())
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
cx_b.update(|cx| {
|
||||
repo_b
|
||||
.read(cx)
|
||||
.change_branch("totally-new-branch".to_string())
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
@@ -276,11 +276,13 @@ async fn test_ssh_collaboration_git_branches(
|
||||
// has some git repositories
|
||||
executor.run_until_parked();
|
||||
|
||||
let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
|
||||
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)))
|
||||
.update(|cx| repo_b.read(cx).branches())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let new_branch = branches[2];
|
||||
@@ -292,13 +294,10 @@ async fn test_ssh_collaboration_git_branches(
|
||||
|
||||
assert_eq!(&branches_b, &branches_set);
|
||||
|
||||
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();
|
||||
cx_b.update(|cx| repo_b.read(cx).change_branch(new_branch.to_string()))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
@@ -318,11 +317,21 @@ async fn test_ssh_collaboration_git_branches(
|
||||
|
||||
// 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)
|
||||
})
|
||||
repo_b
|
||||
.read(cx)
|
||||
.create_branch("totally-new-branch".to_string())
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
cx_b.update(|cx| {
|
||||
repo_b
|
||||
.read(cx)
|
||||
.change_branch("totally-new-branch".to_string())
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
@@ -226,3 +226,7 @@ impl Item for ComponentPreview {
|
||||
f(*event)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: impl serializable item for component preview so it will restore with the workspace
|
||||
// ref: https://github.com/zed-industries/zed/blob/32201ac70a501e63dfa2ade9c00f85aea2d4dd94/crates/image_viewer/src/image_viewer.rs#L199
|
||||
// Use `ImageViewer` as a model for how to do it, except it'll be even simpler
|
||||
|
||||
@@ -196,20 +196,6 @@ pub struct DeleteToPreviousWordStart {
|
||||
pub ignore_newlines: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct GoToHunk {
|
||||
#[serde(default)]
|
||||
pub center_cursor: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct GoToPreviousHunk {
|
||||
#[serde(default)]
|
||||
pub center_cursor: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct FoldAtLevel(pub u32);
|
||||
|
||||
@@ -240,8 +226,6 @@ impl_actions!(
|
||||
ExpandExcerptsDown,
|
||||
ExpandExcerptsUp,
|
||||
FoldAt,
|
||||
GoToHunk,
|
||||
GoToPreviousHunk,
|
||||
HandleInput,
|
||||
MoveDownByLines,
|
||||
MovePageDown,
|
||||
@@ -323,6 +307,8 @@ gpui::actions!(
|
||||
GoToDefinition,
|
||||
GoToDefinitionSplit,
|
||||
GoToDiagnostic,
|
||||
GoToHunk,
|
||||
GoToPreviousHunk,
|
||||
GoToImplementation,
|
||||
GoToImplementationSplit,
|
||||
GoToPreviousDiagnostic,
|
||||
|
||||
@@ -1124,6 +1124,11 @@ impl DisplaySnapshot {
|
||||
self.block_snapshot.is_block_line(BlockRow(display_row.0))
|
||||
}
|
||||
|
||||
pub fn is_folded_buffer_header(&self, display_row: DisplayRow) -> bool {
|
||||
self.block_snapshot
|
||||
.is_folded_buffer_header(BlockRow(display_row.0))
|
||||
}
|
||||
|
||||
pub fn soft_wrap_indent(&self, display_row: DisplayRow) -> Option<u32> {
|
||||
let wrap_row = self
|
||||
.block_snapshot
|
||||
|
||||
@@ -1618,6 +1618,15 @@ impl BlockSnapshot {
|
||||
cursor.item().map_or(false, |t| t.block.is_some())
|
||||
}
|
||||
|
||||
pub(super) fn is_folded_buffer_header(&self, row: BlockRow) -> bool {
|
||||
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
|
||||
cursor.seek(&row, Bias::Right, &());
|
||||
let Some(transform) = cursor.item() else {
|
||||
return false;
|
||||
};
|
||||
matches!(transform.block, Some(Block::FoldedBuffer { .. }))
|
||||
}
|
||||
|
||||
pub(super) fn is_line_replaced(&self, row: MultiBufferRow) -> bool {
|
||||
let wrap_point = self
|
||||
.wrap_snapshot
|
||||
|
||||
@@ -73,7 +73,7 @@ use futures::{
|
||||
};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
|
||||
use ::git::{status::FileStatus, Restore};
|
||||
use ::git::Restore;
|
||||
use code_context_menus::{
|
||||
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
|
||||
CompletionsMenu, ContextMenuOrigin,
|
||||
@@ -2233,6 +2233,49 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn sync_selections(
|
||||
&mut self,
|
||||
other: Entity<Editor>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> gpui::Subscription {
|
||||
let other_selections = other.read(cx).selections.disjoint.to_vec();
|
||||
self.selections.change_with(cx, |selections| {
|
||||
selections.select_anchors(other_selections);
|
||||
});
|
||||
|
||||
let other_subscription =
|
||||
cx.subscribe(&other, |this, other, other_evt, cx| match other_evt {
|
||||
EditorEvent::SelectionsChanged { local: true } => {
|
||||
let other_selections = other.read(cx).selections.disjoint.to_vec();
|
||||
if other_selections.is_empty() {
|
||||
return;
|
||||
}
|
||||
this.selections.change_with(cx, |selections| {
|
||||
selections.select_anchors(other_selections);
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
let this_subscription =
|
||||
cx.subscribe_self::<EditorEvent>(move |this, this_evt, cx| match this_evt {
|
||||
EditorEvent::SelectionsChanged { local: true } => {
|
||||
let these_selections = this.selections.disjoint.to_vec();
|
||||
if these_selections.is_empty() {
|
||||
return;
|
||||
}
|
||||
other.update(cx, |other_editor, cx| {
|
||||
other_editor.selections.change_with(cx, |selections| {
|
||||
selections.select_anchors(these_selections);
|
||||
})
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
Subscription::join(other_subscription, this_subscription)
|
||||
}
|
||||
|
||||
pub fn change_selections<R>(
|
||||
&mut self,
|
||||
autoscroll: Option<Autoscroll>,
|
||||
@@ -7731,7 +7774,7 @@ impl Editor {
|
||||
for hunk in &hunks {
|
||||
self.prepare_restore_change(&mut revert_changes, hunk, cx);
|
||||
}
|
||||
self.do_stage_or_unstage(false, buffer_id, hunks.into_iter(), window, cx);
|
||||
self.do_stage_or_unstage(false, buffer_id, hunks.into_iter(), cx);
|
||||
}
|
||||
drop(chunk_by);
|
||||
if !revert_changes.is_empty() {
|
||||
@@ -11412,14 +11455,13 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn go_to_next_hunk(&mut self, action: &GoToHunk, window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
let selection = self.selections.newest::<Point>(cx);
|
||||
self.go_to_hunk_after_or_before_position(
|
||||
&snapshot,
|
||||
selection.head(),
|
||||
true,
|
||||
action.center_cursor,
|
||||
Direction::Next,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -11429,32 +11471,26 @@ impl Editor {
|
||||
&mut self,
|
||||
snapshot: &EditorSnapshot,
|
||||
position: Point,
|
||||
after: bool,
|
||||
scroll_center: bool,
|
||||
direction: Direction,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Option<MultiBufferDiffHunk> {
|
||||
let hunk = if after {
|
||||
) {
|
||||
let row = if direction == Direction::Next {
|
||||
self.hunk_after_position(snapshot, position)
|
||||
.map(|hunk| hunk.row_range.start)
|
||||
} else {
|
||||
self.hunk_before_position(snapshot, position)
|
||||
};
|
||||
|
||||
if let Some(hunk) = &hunk {
|
||||
let destination = Point::new(hunk.row_range.start.0, 0);
|
||||
let autoscroll = if scroll_center {
|
||||
Autoscroll::center()
|
||||
} else {
|
||||
Autoscroll::fit()
|
||||
};
|
||||
if let Some(row) = row {
|
||||
let destination = Point::new(row.0, 0);
|
||||
let autoscroll = Autoscroll::center();
|
||||
|
||||
self.unfold_ranges(&[destination..destination], false, false, cx);
|
||||
self.change_selections(Some(autoscroll), window, cx, |s| {
|
||||
s.select_ranges([destination..destination]);
|
||||
});
|
||||
}
|
||||
|
||||
hunk
|
||||
}
|
||||
|
||||
fn hunk_after_position(
|
||||
@@ -11476,7 +11512,7 @@ impl Editor {
|
||||
|
||||
fn go_to_prev_hunk(
|
||||
&mut self,
|
||||
action: &GoToPreviousHunk,
|
||||
_: &GoToPreviousHunk,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -11485,8 +11521,7 @@ impl Editor {
|
||||
self.go_to_hunk_after_or_before_position(
|
||||
&snapshot,
|
||||
selection.head(),
|
||||
false,
|
||||
action.center_cursor,
|
||||
Direction::Prev,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -11496,7 +11531,7 @@ impl Editor {
|
||||
&mut self,
|
||||
snapshot: &EditorSnapshot,
|
||||
position: Point,
|
||||
) -> Option<MultiBufferDiffHunk> {
|
||||
) -> Option<MultiBufferRow> {
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.diff_hunk_before(position)
|
||||
@@ -12965,13 +13000,18 @@ impl Editor {
|
||||
}
|
||||
} else {
|
||||
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let buffer_ids: HashSet<_> = multi_buffer_snapshot
|
||||
.ranges_to_buffer_ranges(self.selections.disjoint_anchor_ranges())
|
||||
.map(|(snapshot, _, _)| snapshot.remote_id())
|
||||
let buffer_ids: HashSet<_> = self
|
||||
.selections
|
||||
.disjoint_anchor_ranges()
|
||||
.flat_map(|range| multi_buffer_snapshot.buffer_ids_for_range(range))
|
||||
.collect();
|
||||
|
||||
let should_unfold = buffer_ids
|
||||
.iter()
|
||||
.any(|buffer_id| self.is_buffer_folded(*buffer_id, cx));
|
||||
|
||||
for buffer_id in buffer_ids {
|
||||
if self.is_buffer_folded(buffer_id, cx) {
|
||||
if should_unfold {
|
||||
self.unfold_buffer(buffer_id, cx);
|
||||
} else {
|
||||
self.fold_buffer(buffer_id, cx);
|
||||
@@ -13547,148 +13587,116 @@ impl Editor {
|
||||
pub fn toggle_staged_selected_diff_hunks(
|
||||
&mut self,
|
||||
_: &::git::ToggleStaged,
|
||||
window: &mut Window,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect();
|
||||
let stage = self.has_stageable_diff_hunks_in_ranges(&ranges, &snapshot);
|
||||
self.stage_or_unstage_diff_hunks(stage, &ranges, window, cx);
|
||||
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
|
||||
}
|
||||
|
||||
pub fn stage_and_next(
|
||||
&mut self,
|
||||
action: &::git::StageAndNext,
|
||||
_: &::git::StageAndNext,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.do_stage_or_unstage_and_next(true, action.whole_excerpt, window, cx);
|
||||
self.do_stage_or_unstage_and_next(true, window, cx);
|
||||
}
|
||||
|
||||
pub fn unstage_and_next(
|
||||
&mut self,
|
||||
action: &::git::UnstageAndNext,
|
||||
_: &::git::UnstageAndNext,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.do_stage_or_unstage_and_next(false, action.whole_excerpt, window, cx);
|
||||
self.do_stage_or_unstage_and_next(false, window, cx);
|
||||
}
|
||||
|
||||
pub fn stage_or_unstage_diff_hunks(
|
||||
&mut self,
|
||||
stage: bool,
|
||||
ranges: &[Range<Anchor>],
|
||||
window: &mut Window,
|
||||
ranges: Vec<Range<Anchor>>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let chunk_by = self
|
||||
.diff_hunks_in_ranges(&ranges, &snapshot)
|
||||
.chunk_by(|hunk| hunk.buffer_id);
|
||||
for (buffer_id, hunks) in &chunk_by {
|
||||
self.do_stage_or_unstage(stage, buffer_id, hunks, window, cx);
|
||||
let task = self.save_buffers_for_ranges_if_needed(&ranges, cx);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
task.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let snapshot = this.buffer.read(cx).snapshot(cx);
|
||||
let chunk_by = this
|
||||
.diff_hunks_in_ranges(&ranges, &snapshot)
|
||||
.chunk_by(|hunk| hunk.buffer_id);
|
||||
for (buffer_id, hunks) in &chunk_by {
|
||||
this.do_stage_or_unstage(stage, buffer_id, hunks, cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn save_buffers_for_ranges_if_needed(
|
||||
&mut self,
|
||||
ranges: &[Range<Anchor>],
|
||||
cx: &mut Context<'_, Editor>,
|
||||
) -> Task<Result<()>> {
|
||||
let multibuffer = self.buffer.read(cx);
|
||||
let snapshot = multibuffer.read(cx);
|
||||
let buffer_ids: HashSet<_> = ranges
|
||||
.iter()
|
||||
.flat_map(|range| snapshot.buffer_ids_for_range(range.clone()))
|
||||
.collect();
|
||||
drop(snapshot);
|
||||
|
||||
let mut buffers = HashSet::default();
|
||||
for buffer_id in buffer_ids {
|
||||
if let Some(buffer_entity) = multibuffer.buffer(buffer_id) {
|
||||
let buffer = buffer_entity.read(cx);
|
||||
if buffer.file().is_some_and(|file| file.disk_state().exists()) && buffer.is_dirty()
|
||||
{
|
||||
buffers.insert(buffer_entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(project) = &self.project {
|
||||
project.update(cx, |project, cx| project.save_buffers(buffers, cx))
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
fn do_stage_or_unstage_and_next(
|
||||
&mut self,
|
||||
stage: bool,
|
||||
whole_excerpt: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let mut ranges = self.selections.disjoint_anchor_ranges().collect::<Vec<_>>();
|
||||
let ranges = self.selections.disjoint_anchor_ranges().collect::<Vec<_>>();
|
||||
|
||||
if ranges.iter().any(|range| range.start != range.end) {
|
||||
self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx);
|
||||
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
if !whole_excerpt {
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
let newest_range = self.selections.newest::<Point>(cx).range();
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
let newest_range = self.selections.newest::<Point>(cx).range();
|
||||
|
||||
let run_twice = snapshot
|
||||
.hunks_for_ranges([newest_range])
|
||||
.first()
|
||||
.is_some_and(|hunk| {
|
||||
let next_line = Point::new(hunk.row_range.end.0 + 1, 0);
|
||||
self.hunk_after_position(&snapshot, next_line)
|
||||
.is_some_and(|other| other.row_range == hunk.row_range)
|
||||
});
|
||||
let run_twice = snapshot
|
||||
.hunks_for_ranges([newest_range])
|
||||
.first()
|
||||
.is_some_and(|hunk| {
|
||||
let next_line = Point::new(hunk.row_range.end.0 + 1, 0);
|
||||
self.hunk_after_position(&snapshot, next_line)
|
||||
.is_some_and(|other| other.row_range == hunk.row_range)
|
||||
});
|
||||
|
||||
if run_twice {
|
||||
self.go_to_next_hunk(
|
||||
&GoToHunk {
|
||||
center_cursor: true,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
} else if !self.buffer().read(cx).is_singleton() {
|
||||
self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx);
|
||||
|
||||
if let Some((excerpt_id, buffer, range)) = self.active_excerpt(cx) {
|
||||
if buffer.read(cx).is_empty() {
|
||||
let buffer = buffer.read(cx);
|
||||
let Some(file) = buffer.file() else {
|
||||
return;
|
||||
};
|
||||
let project_path = project::ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
path: file.path().clone(),
|
||||
};
|
||||
let Some(project) = self.project.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(repo) = project.read(cx).git_store().read(cx).active_repository()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
repo.update(cx, |repo, cx| {
|
||||
let Some(repo_path) = repo.project_path_to_repo_path(&project_path) else {
|
||||
return;
|
||||
};
|
||||
let Some(status) = repo.repository_entry.status_for_path(&repo_path) else {
|
||||
return;
|
||||
};
|
||||
if stage && status.status == FileStatus::Untracked {
|
||||
repo.stage_entries(vec![repo_path], cx)
|
||||
.detach_and_log_err(cx);
|
||||
return;
|
||||
}
|
||||
})
|
||||
}
|
||||
ranges = vec![multi_buffer::Anchor::range_in_buffer(
|
||||
excerpt_id,
|
||||
buffer.read(cx).remote_id(),
|
||||
range,
|
||||
)];
|
||||
self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx);
|
||||
let snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
let mut point = ranges.last().unwrap().end.to_point(&snapshot);
|
||||
if point.row < snapshot.max_row().0 {
|
||||
point.row += 1;
|
||||
point.column = 0;
|
||||
point = snapshot.clip_point(point, Bias::Right);
|
||||
self.change_selections(Some(Autoscroll::top_relative(6)), window, cx, |s| {
|
||||
s.select_ranges([point..point]);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if run_twice {
|
||||
self.go_to_next_hunk(&GoToHunk, window, cx);
|
||||
}
|
||||
self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx);
|
||||
self.go_to_next_hunk(
|
||||
&GoToHunk {
|
||||
center_cursor: true,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
|
||||
self.go_to_next_hunk(&GoToHunk, window, cx);
|
||||
}
|
||||
|
||||
fn do_stage_or_unstage(
|
||||
@@ -13696,31 +13704,16 @@ impl Editor {
|
||||
stage: bool,
|
||||
buffer_id: BufferId,
|
||||
hunks: impl Iterator<Item = MultiBufferDiffHunk>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let Some(project) = self.project.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else {
|
||||
return;
|
||||
};
|
||||
let Some(diff) = self.buffer.read(cx).diff_for(buffer_id) else {
|
||||
return;
|
||||
};
|
||||
) -> Option<()> {
|
||||
let project = self.project.as_ref()?;
|
||||
let buffer = project.read(cx).buffer_for_id(buffer_id, cx)?;
|
||||
let diff = self.buffer.read(cx).diff_for(buffer_id)?;
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let file_exists = buffer_snapshot
|
||||
.file()
|
||||
.is_some_and(|file| file.disk_state().exists());
|
||||
let Some((repo, path)) = project
|
||||
.read(cx)
|
||||
.repository_and_path_for_buffer_id(buffer_id, cx)
|
||||
else {
|
||||
log::debug!("no git repo for buffer id");
|
||||
return;
|
||||
};
|
||||
|
||||
let new_index_text = diff.update(cx, |diff, cx| {
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.stage_or_unstage_hunks(
|
||||
stage,
|
||||
&hunks
|
||||
@@ -13728,7 +13721,7 @@ impl Editor {
|
||||
buffer_range: hunk.buffer_range,
|
||||
diff_base_byte_range: hunk.diff_base_byte_range,
|
||||
secondary_status: hunk.secondary_status,
|
||||
row_range: 0..0, // unused
|
||||
range: Point::zero()..Point::zero(), // unused
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
&buffer_snapshot,
|
||||
@@ -13736,20 +13729,7 @@ impl Editor {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
if file_exists {
|
||||
let buffer_store = project.read(cx).buffer_store().clone();
|
||||
buffer_store
|
||||
.update(cx, |buffer_store, cx| buffer_store.save_buffer(buffer, cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
let recv = repo
|
||||
.read(cx)
|
||||
.set_index_text(&path, new_index_text.map(|rope| rope.to_string()));
|
||||
|
||||
cx.background_spawn(async move { recv.await? })
|
||||
.detach_and_notify_err(window, cx);
|
||||
None
|
||||
}
|
||||
|
||||
pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context<Self>) {
|
||||
@@ -16036,9 +16016,9 @@ impl Editor {
|
||||
if let Some(buffer) = multi_buffer.buffer(buffer_id) {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
changes.into_iter().map(|(range, text)| {
|
||||
(range, text.to_string().map(Arc::<str>::from))
|
||||
}),
|
||||
changes
|
||||
.into_iter()
|
||||
.map(|(range, text)| (range, text.to_string())),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
@@ -16260,7 +16240,7 @@ fn get_uncommitted_diff_for_buffer(
|
||||
}
|
||||
});
|
||||
cx.spawn(|mut cx| async move {
|
||||
let diffs = futures::future::join_all(tasks).await;
|
||||
let diffs = future::join_all(tasks).await;
|
||||
buffer
|
||||
.update(&mut cx, |buffer, cx| {
|
||||
for diff in diffs.into_iter().flatten() {
|
||||
@@ -17156,17 +17136,14 @@ impl EditorSnapshot {
|
||||
for hunk in self.buffer_snapshot.diff_hunks_in_range(
|
||||
Point::new(query_rows.start.0, 0)..Point::new(query_rows.end.0, 0),
|
||||
) {
|
||||
// Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it
|
||||
// when the caret is just above or just below the deleted hunk.
|
||||
let allow_adjacent = hunk.status().is_deleted();
|
||||
let related_to_selection = if allow_adjacent {
|
||||
hunk.row_range.overlaps(&query_rows)
|
||||
|| hunk.row_range.start == query_rows.end
|
||||
|| hunk.row_range.end == query_rows.start
|
||||
} else {
|
||||
hunk.row_range.overlaps(&query_rows)
|
||||
};
|
||||
if related_to_selection {
|
||||
// Include deleted hunks that are adjacent to the query range, because
|
||||
// otherwise they would be missed.
|
||||
let mut intersects_range = hunk.row_range.overlaps(&query_rows);
|
||||
if hunk.status().is_deleted() {
|
||||
intersects_range |= hunk.row_range.start == query_rows.end;
|
||||
intersects_range |= hunk.row_range.end == query_rows.start;
|
||||
}
|
||||
if intersects_range {
|
||||
if !processed_buffer_rows
|
||||
.entry(hunk.buffer_id)
|
||||
.or_default()
|
||||
|
||||
@@ -11413,7 +11413,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext)
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
//Wrap around the bottom of the buffer
|
||||
for _ in 0..3 {
|
||||
editor.go_to_next_hunk(&GoToHunk::default(), window, cx);
|
||||
editor.go_to_next_hunk(&GoToHunk, window, cx);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11435,7 +11435,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext)
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
//Wrap around the top of the buffer
|
||||
for _ in 0..2 {
|
||||
editor.go_to_prev_hunk(&GoToPreviousHunk::default(), window, cx);
|
||||
editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11455,7 +11455,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext)
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.go_to_prev_hunk(&GoToPreviousHunk::default(), window, cx);
|
||||
editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
|
||||
});
|
||||
|
||||
cx.assert_editor_state(
|
||||
@@ -11474,7 +11474,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext)
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.go_to_prev_hunk(&GoToPreviousHunk::default(), window, cx);
|
||||
editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
|
||||
});
|
||||
|
||||
cx.assert_editor_state(
|
||||
@@ -11494,7 +11494,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext)
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
for _ in 0..2 {
|
||||
editor.go_to_prev_hunk(&GoToPreviousHunk::default(), window, cx);
|
||||
editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11518,7 +11518,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext)
|
||||
});
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.go_to_next_hunk(&GoToHunk::default(), window, cx);
|
||||
editor.go_to_next_hunk(&GoToHunk, window, cx);
|
||||
});
|
||||
|
||||
cx.assert_editor_state(
|
||||
@@ -13525,7 +13525,7 @@ async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut
|
||||
executor.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.go_to_next_hunk(&GoToHunk::default(), window, cx);
|
||||
editor.go_to_next_hunk(&GoToHunk, window, cx);
|
||||
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
@@ -13547,7 +13547,7 @@ async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
for _ in 0..2 {
|
||||
editor.go_to_next_hunk(&GoToHunk::default(), window, cx);
|
||||
editor.go_to_next_hunk(&GoToHunk, window, cx);
|
||||
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
|
||||
}
|
||||
});
|
||||
@@ -13570,7 +13570,7 @@ async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.go_to_next_hunk(&GoToHunk::default(), window, cx);
|
||||
editor.go_to_next_hunk(&GoToHunk, window, cx);
|
||||
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
|
||||
@@ -32,15 +32,17 @@ use collections::{BTreeMap, HashMap, HashSet};
|
||||
use file_icons::FileIcons;
|
||||
use git::{blame::BlameEntry, status::FileStatus, Oid};
|
||||
use gpui::{
|
||||
anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad,
|
||||
relative, size, svg, transparent_black, Action, AnyElement, App, AvailableSpace, Axis, Bounds,
|
||||
ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase,
|
||||
Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox,
|
||||
Hsla, InteractiveElement, IntoElement, Keystroke, Length, ModifiersChangedEvent, MouseButton,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
|
||||
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
|
||||
Subscription, TextRun, TextStyleRefinement, Window,
|
||||
anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, pattern_slash,
|
||||
point, px, quad, relative, size, solid_background, svg, transparent_black, Action, AnyElement,
|
||||
App, AvailableSpace, Axis, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner,
|
||||
Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
|
||||
Focusable as _, FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement,
|
||||
Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||
MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine,
|
||||
SharedString, Size, StatefulInteractiveElement, Style, Styled, Subscription, TextRun,
|
||||
TextStyleRefinement, Window,
|
||||
};
|
||||
use inline_completion::Direction;
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
language_settings::{
|
||||
@@ -54,7 +56,7 @@ use multi_buffer::{
|
||||
Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
|
||||
RowInfo,
|
||||
};
|
||||
use project::project_settings::{self, GitGutterSetting, ProjectSettings};
|
||||
use project::project_settings::{self, GitGutterSetting, GitHunkStyleSetting, ProjectSettings};
|
||||
use settings::Settings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use std::{
|
||||
@@ -75,7 +77,7 @@ use ui::{
|
||||
POPOVER_Y_PADDING,
|
||||
};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use util::{debug_panic, maybe, RangeExt, ResultExt};
|
||||
use util::{debug_panic, RangeExt, ResultExt};
|
||||
use workspace::{item::Item, notifications::NotifyTaskExt};
|
||||
|
||||
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;
|
||||
@@ -2016,7 +2018,7 @@ impl EditorElement {
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
gutter_hitbox: &Hitbox,
|
||||
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
|
||||
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
|
||||
snapshot: &EditorSnapshot,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -2092,7 +2094,7 @@ impl EditorElement {
|
||||
gutter_dimensions,
|
||||
scroll_pixel_position,
|
||||
gutter_hitbox,
|
||||
rows_with_hunk_bounds,
|
||||
display_hunks,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -2110,7 +2112,7 @@ impl EditorElement {
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
gutter_hitbox: &Hitbox,
|
||||
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
|
||||
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyElement> {
|
||||
@@ -2135,7 +2137,7 @@ impl EditorElement {
|
||||
gutter_dimensions,
|
||||
scroll_pixel_position,
|
||||
gutter_hitbox,
|
||||
rows_with_hunk_bounds,
|
||||
display_hunks,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -2674,24 +2676,21 @@ impl EditorElement {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Div {
|
||||
let file_status = maybe!({
|
||||
let project = self.editor.read(cx).project.as_ref()?.read(cx);
|
||||
let (repo, path) =
|
||||
project.repository_and_path_for_buffer_id(for_excerpt.buffer_id, cx)?;
|
||||
let status = repo.read(cx).repository_entry.status_for_path(&path)?;
|
||||
Some(status.status)
|
||||
})
|
||||
.filter(|_| {
|
||||
self.editor
|
||||
.read(cx)
|
||||
.buffer
|
||||
.read(cx)
|
||||
.all_diff_hunks_expanded()
|
||||
});
|
||||
|
||||
let include_root = self
|
||||
.editor
|
||||
let editor = self.editor.read(cx);
|
||||
let file_status = editor
|
||||
.buffer
|
||||
.read(cx)
|
||||
.all_diff_hunks_expanded()
|
||||
.then(|| {
|
||||
editor
|
||||
.project
|
||||
.as_ref()?
|
||||
.read(cx)
|
||||
.status_for_buffer_id(for_excerpt.buffer_id, cx)
|
||||
})
|
||||
.flatten();
|
||||
|
||||
let include_root = editor
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
|
||||
@@ -2703,7 +2702,7 @@ impl EditorElement {
|
||||
let parent_path = path.as_ref().and_then(|path| {
|
||||
Some(path.parent()?.to_string_lossy().to_string() + std::path::MAIN_SEPARATOR_STR)
|
||||
});
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
div()
|
||||
@@ -2722,7 +2721,10 @@ impl EditorElement {
|
||||
.shadow_md()
|
||||
.border_1()
|
||||
.map(|div| {
|
||||
let border_color = if is_selected && is_folded {
|
||||
let border_color = if is_selected
|
||||
&& is_folded
|
||||
&& focus_handle.contains_focused(window, cx)
|
||||
{
|
||||
colors.border_focused
|
||||
} else {
|
||||
colors.border
|
||||
@@ -2773,8 +2775,7 @@ impl EditorElement {
|
||||
)
|
||||
})
|
||||
.children(
|
||||
self.editor
|
||||
.read(cx)
|
||||
editor
|
||||
.addons
|
||||
.values()
|
||||
.filter_map(|addon| {
|
||||
@@ -4343,7 +4344,7 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_diff_hunks(layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
|
||||
fn paint_gutter_diff_hunks(layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
|
||||
let is_light = cx.theme().appearance().is_light();
|
||||
|
||||
if layout.display_hunks.is_empty() {
|
||||
@@ -4413,10 +4414,19 @@ impl EditorElement {
|
||||
background_color =
|
||||
background_color.opacity(if is_light { 0.2 } else { 0.32 });
|
||||
}
|
||||
|
||||
// Flatten the background color with the editor color to prevent
|
||||
// elements below transparent hunks from showing through
|
||||
let flattened_background_color = cx
|
||||
.theme()
|
||||
.colors()
|
||||
.editor_background
|
||||
.blend(background_color);
|
||||
|
||||
window.paint_quad(quad(
|
||||
hunk_bounds,
|
||||
corner_radii,
|
||||
background_color,
|
||||
flattened_background_color,
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
));
|
||||
@@ -4544,7 +4554,7 @@ impl EditorElement {
|
||||
)
|
||||
});
|
||||
if show_git_gutter {
|
||||
Self::paint_diff_hunks(layout, window, cx)
|
||||
Self::paint_gutter_diff_hunks(layout, window, cx)
|
||||
}
|
||||
|
||||
let highlight_width = 0.275 * layout.position_map.line_height;
|
||||
@@ -5675,7 +5685,7 @@ fn prepaint_gutter_button(
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_hitbox: &Hitbox,
|
||||
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
|
||||
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
@@ -5687,9 +5697,23 @@ fn prepaint_gutter_button(
|
||||
let indicator_size = button.layout_as_root(available_space, window, cx);
|
||||
|
||||
let blame_width = gutter_dimensions.git_blame_entries_width;
|
||||
let gutter_width = rows_with_hunk_bounds
|
||||
.get(&row)
|
||||
.map(|bounds| bounds.size.width);
|
||||
let gutter_width = display_hunks
|
||||
.binary_search_by(|(hunk, _)| match hunk {
|
||||
DisplayDiffHunk::Folded { display_row } => display_row.cmp(&row),
|
||||
DisplayDiffHunk::Unfolded {
|
||||
display_row_range, ..
|
||||
} => {
|
||||
if display_row_range.end <= row {
|
||||
Ordering::Less
|
||||
} else if display_row_range.start > row {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
.and_then(|ix| Some(display_hunks[ix].1.as_ref()?.size.width));
|
||||
let left_offset = blame_width.max(gutter_width).unwrap_or_default();
|
||||
|
||||
let mut x = left_offset;
|
||||
@@ -6708,15 +6732,16 @@ impl Element for EditorElement {
|
||||
.update(cx, |editor, cx| editor.highlighted_display_rows(window, cx));
|
||||
|
||||
let is_light = cx.theme().appearance().is_light();
|
||||
let use_pattern = ProjectSettings::get_global(cx)
|
||||
.git
|
||||
.hunk_style
|
||||
.map_or(false, |style| matches!(style, GitHunkStyleSetting::Pattern));
|
||||
|
||||
for (ix, row_info) in row_infos.iter().enumerate() {
|
||||
let Some(diff_status) = row_info.diff_status else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let staged_opacity = if is_light { 0.14 } else { 0.10 };
|
||||
let unstaged_opacity = 0.04;
|
||||
|
||||
let background_color = match diff_status.kind {
|
||||
DiffHunkStatusKind::Added => cx.theme().colors().version_control_added,
|
||||
DiffHunkStatusKind::Deleted => {
|
||||
@@ -6727,15 +6752,34 @@ impl Element for EditorElement {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let background_color = if diff_status.has_secondary_hunk() {
|
||||
background_color.opacity(unstaged_opacity)
|
||||
|
||||
let unstaged = diff_status.has_secondary_hunk();
|
||||
let hunk_opacity = if is_light { 0.16 } else { 0.12 };
|
||||
|
||||
let staged_background =
|
||||
solid_background(background_color.opacity(hunk_opacity));
|
||||
let unstaged_background = if use_pattern {
|
||||
pattern_slash(
|
||||
background_color.opacity(hunk_opacity),
|
||||
window.rem_size().0 * 1.125, // ~18 by default
|
||||
)
|
||||
} else {
|
||||
background_color.opacity(staged_opacity)
|
||||
solid_background(background_color.opacity(if is_light {
|
||||
0.08
|
||||
} else {
|
||||
0.04
|
||||
}))
|
||||
};
|
||||
|
||||
let background = if unstaged {
|
||||
unstaged_background
|
||||
} else {
|
||||
staged_background
|
||||
};
|
||||
|
||||
highlighted_rows
|
||||
.entry(start_row + DisplayRow(ix as u32))
|
||||
.or_insert(background_color.into());
|
||||
.or_insert(background);
|
||||
}
|
||||
|
||||
let highlighted_ranges = self.editor.read(cx).background_highlights_in_range(
|
||||
@@ -7185,27 +7229,6 @@ impl Element for EditorElement {
|
||||
|
||||
let gutter_settings = EditorSettings::get_global(cx).gutter;
|
||||
|
||||
let rows_with_hunk_bounds = display_hunks
|
||||
.iter()
|
||||
.filter_map(|(hunk, hitbox)| Some((hunk, hitbox.as_ref()?.bounds)))
|
||||
.fold(
|
||||
HashMap::default(),
|
||||
|mut rows_with_hunk_bounds, (hunk, bounds)| {
|
||||
match hunk {
|
||||
DisplayDiffHunk::Folded { display_row } => {
|
||||
rows_with_hunk_bounds.insert(*display_row, bounds);
|
||||
}
|
||||
DisplayDiffHunk::Unfolded {
|
||||
display_row_range, ..
|
||||
} => {
|
||||
for display_row in display_row_range.iter_rows() {
|
||||
rows_with_hunk_bounds.insert(display_row, bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
rows_with_hunk_bounds
|
||||
},
|
||||
);
|
||||
let mut code_actions_indicator = None;
|
||||
if let Some(newest_selection_head) = newest_selection_head {
|
||||
let newest_selection_point =
|
||||
@@ -7255,7 +7278,7 @@ impl Element for EditorElement {
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
&rows_with_hunk_bounds,
|
||||
&display_hunks,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -7283,7 +7306,7 @@ impl Element for EditorElement {
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
&rows_with_hunk_bounds,
|
||||
&display_hunks,
|
||||
&snapshot,
|
||||
window,
|
||||
cx,
|
||||
@@ -8795,12 +8818,11 @@ fn diff_hunk_controls(
|
||||
})
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
move |_event, window, cx| {
|
||||
move |_event, _window, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.stage_or_unstage_diff_hunks(
|
||||
true,
|
||||
&[hunk_range.start..hunk_range.start],
|
||||
window,
|
||||
vec![hunk_range.start..hunk_range.start],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -8823,12 +8845,11 @@ fn diff_hunk_controls(
|
||||
})
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
move |_event, window, cx| {
|
||||
move |_event, _window, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.stage_or_unstage_diff_hunks(
|
||||
false,
|
||||
&[hunk_range.start..hunk_range.start],
|
||||
window,
|
||||
vec![hunk_range.start..hunk_range.start],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -8873,7 +8894,7 @@ fn diff_hunk_controls(
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Next Hunk",
|
||||
&GoToHunk::default(),
|
||||
&GoToHunk,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
@@ -8888,7 +8909,11 @@ fn diff_hunk_controls(
|
||||
let position =
|
||||
hunk_range.end.to_point(&snapshot.buffer_snapshot);
|
||||
editor.go_to_hunk_after_or_before_position(
|
||||
&snapshot, position, true, true, window, cx,
|
||||
&snapshot,
|
||||
position,
|
||||
Direction::Next,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.expand_selected_diff_hunks(cx);
|
||||
});
|
||||
@@ -8905,7 +8930,7 @@ fn diff_hunk_controls(
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Previous Hunk",
|
||||
&GoToPreviousHunk::default(),
|
||||
&GoToPreviousHunk,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
@@ -8920,7 +8945,11 @@ fn diff_hunk_controls(
|
||||
let point =
|
||||
hunk_range.start.to_point(&snapshot.buffer_snapshot);
|
||||
editor.go_to_hunk_after_or_before_position(
|
||||
&snapshot, point, false, true, window, cx,
|
||||
&snapshot,
|
||||
point,
|
||||
Direction::Prev,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.expand_selected_diff_hunks(cx);
|
||||
});
|
||||
|
||||
@@ -195,9 +195,12 @@ impl GitBlame {
|
||||
) -> impl 'a + Iterator<Item = Option<BlameEntry>> {
|
||||
self.sync(cx);
|
||||
|
||||
let buffer_id = self.buffer_snapshot.remote_id();
|
||||
let mut cursor = self.entries.cursor::<u32>(&());
|
||||
rows.into_iter().map(move |info| {
|
||||
let row = info.buffer_row?;
|
||||
let row = info
|
||||
.buffer_row
|
||||
.filter(|_| info.buffer_id == Some(buffer_id))?;
|
||||
cursor.seek_forward(&row, Bias::Right, &());
|
||||
cursor.item()?.blame.clone()
|
||||
})
|
||||
@@ -535,6 +538,7 @@ mod tests {
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{cmp, env, ops::Range, path::Path};
|
||||
use text::BufferId;
|
||||
use unindent::Unindent as _;
|
||||
use util::{path, RandomCharIter};
|
||||
|
||||
@@ -552,16 +556,18 @@ mod tests {
|
||||
#[track_caller]
|
||||
fn assert_blame_rows(
|
||||
blame: &mut GitBlame,
|
||||
buffer_id: BufferId,
|
||||
rows: Range<u32>,
|
||||
expected: Vec<Option<BlameEntry>>,
|
||||
cx: &mut Context<GitBlame>,
|
||||
) {
|
||||
assert_eq!(
|
||||
pretty_assertions::assert_eq!(
|
||||
blame
|
||||
.blame_for_rows(
|
||||
&rows
|
||||
.map(|row| RowInfo {
|
||||
buffer_row: Some(row),
|
||||
buffer_id: Some(buffer_id),
|
||||
..Default::default()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -694,6 +700,7 @@ mod tests {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id());
|
||||
|
||||
let git_blame = cx.new(|cx| GitBlame::new(buffer.clone(), project, false, true, cx));
|
||||
|
||||
@@ -701,12 +708,13 @@ mod tests {
|
||||
|
||||
git_blame.update(cx, |blame, cx| {
|
||||
// All lines
|
||||
assert_eq!(
|
||||
pretty_assertions::assert_eq!(
|
||||
blame
|
||||
.blame_for_rows(
|
||||
&(0..8)
|
||||
.map(|buffer_row| RowInfo {
|
||||
buffer_row: Some(buffer_row),
|
||||
buffer_id: Some(buffer_id),
|
||||
..Default::default()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -725,12 +733,13 @@ mod tests {
|
||||
]
|
||||
);
|
||||
// Subset of lines
|
||||
assert_eq!(
|
||||
pretty_assertions::assert_eq!(
|
||||
blame
|
||||
.blame_for_rows(
|
||||
&(1..4)
|
||||
.map(|buffer_row| RowInfo {
|
||||
buffer_row: Some(buffer_row),
|
||||
buffer_id: Some(buffer_id),
|
||||
..Default::default()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -744,12 +753,13 @@ mod tests {
|
||||
]
|
||||
);
|
||||
// Subset of lines, with some not displayed
|
||||
assert_eq!(
|
||||
pretty_assertions::assert_eq!(
|
||||
blame
|
||||
.blame_for_rows(
|
||||
&[
|
||||
RowInfo {
|
||||
buffer_row: Some(1),
|
||||
buffer_id: Some(buffer_id),
|
||||
..Default::default()
|
||||
},
|
||||
Default::default(),
|
||||
@@ -800,6 +810,7 @@ mod tests {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id());
|
||||
|
||||
let git_blame = cx.new(|cx| GitBlame::new(buffer.clone(), project, false, true, cx));
|
||||
|
||||
@@ -810,6 +821,7 @@ mod tests {
|
||||
// lines.
|
||||
assert_blame_rows(
|
||||
blame,
|
||||
buffer_id,
|
||||
0..4,
|
||||
vec![
|
||||
Some(blame_entry("1b1b1b", 0..4)),
|
||||
@@ -828,6 +840,7 @@ mod tests {
|
||||
git_blame.update(cx, |blame, cx| {
|
||||
assert_blame_rows(
|
||||
blame,
|
||||
buffer_id,
|
||||
0..2,
|
||||
vec![None, Some(blame_entry("1b1b1b", 0..4))],
|
||||
cx,
|
||||
@@ -840,6 +853,7 @@ mod tests {
|
||||
git_blame.update(cx, |blame, cx| {
|
||||
assert_blame_rows(
|
||||
blame,
|
||||
buffer_id,
|
||||
1..4,
|
||||
vec![
|
||||
None,
|
||||
@@ -852,7 +866,13 @@ mod tests {
|
||||
|
||||
// Before we insert a newline at the end, sanity check:
|
||||
git_blame.update(cx, |blame, cx| {
|
||||
assert_blame_rows(blame, 3..4, vec![Some(blame_entry("1b1b1b", 0..4))], cx);
|
||||
assert_blame_rows(
|
||||
blame,
|
||||
buffer_id,
|
||||
3..4,
|
||||
vec![Some(blame_entry("1b1b1b", 0..4))],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
// Insert a newline at the end
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
@@ -862,6 +882,7 @@ mod tests {
|
||||
git_blame.update(cx, |blame, cx| {
|
||||
assert_blame_rows(
|
||||
blame,
|
||||
buffer_id,
|
||||
3..5,
|
||||
vec![Some(blame_entry("1b1b1b", 0..4)), None],
|
||||
cx,
|
||||
@@ -870,7 +891,13 @@ mod tests {
|
||||
|
||||
// Before we insert a newline at the start, sanity check:
|
||||
git_blame.update(cx, |blame, cx| {
|
||||
assert_blame_rows(blame, 2..3, vec![Some(blame_entry("1b1b1b", 0..4))], cx);
|
||||
assert_blame_rows(
|
||||
blame,
|
||||
buffer_id,
|
||||
2..3,
|
||||
vec![Some(blame_entry("1b1b1b", 0..4))],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Usage example
|
||||
@@ -882,6 +909,7 @@ mod tests {
|
||||
git_blame.update(cx, |blame, cx| {
|
||||
assert_blame_rows(
|
||||
blame,
|
||||
buffer_id,
|
||||
2..4,
|
||||
vec![None, Some(blame_entry("1b1b1b", 0..4))],
|
||||
cx,
|
||||
|
||||
@@ -12,7 +12,7 @@ use gpui::{
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{Buffer, BufferSnapshot, LanguageRegistry};
|
||||
use multi_buffer::{ExcerptRange, MultiBufferRow};
|
||||
use multi_buffer::{Anchor, ExcerptRange, MultiBufferRow};
|
||||
use parking_lot::RwLock;
|
||||
use project::{FakeFs, Project};
|
||||
use std::{
|
||||
@@ -89,6 +89,16 @@ impl EditorTestContext {
|
||||
Path::new("/root")
|
||||
}
|
||||
|
||||
pub async fn for_editor_in(editor: Entity<Editor>, cx: &mut gpui::VisualTestContext) -> Self {
|
||||
cx.focus(&editor);
|
||||
Self {
|
||||
window: cx.windows()[0],
|
||||
cx: cx.clone(),
|
||||
editor,
|
||||
assertion_cx: AssertionContextManager::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn for_editor(editor: WindowHandle<Editor>, cx: &mut gpui::TestAppContext) -> Self {
|
||||
let editor_view = editor.root(cx).unwrap();
|
||||
Self {
|
||||
@@ -381,6 +391,85 @@ impl EditorTestContext {
|
||||
assert_state_with_diff(&self.editor, &mut self.cx, &expected_diff_text);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn assert_excerpts_with_selections(&mut self, marked_text: &str) {
|
||||
let expected_excerpts = marked_text
|
||||
.strip_prefix("[EXCERPT]\n")
|
||||
.unwrap()
|
||||
.split("[EXCERPT]\n")
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (multibuffer_snapshot, selections, excerpts) = self.update_editor(|editor, _, cx| {
|
||||
let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
|
||||
|
||||
let selections = editor.selections.disjoint_anchors();
|
||||
let excerpts = multibuffer_snapshot
|
||||
.excerpts()
|
||||
.map(|(e_id, snapshot, range)| (e_id, snapshot.clone(), range))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(multibuffer_snapshot, selections, excerpts)
|
||||
});
|
||||
|
||||
assert!(
|
||||
excerpts.len() == expected_excerpts.len(),
|
||||
"should have {} excerpts, got {}",
|
||||
expected_excerpts.len(),
|
||||
excerpts.len()
|
||||
);
|
||||
|
||||
for (ix, (excerpt_id, snapshot, range)) in excerpts.into_iter().enumerate() {
|
||||
let is_folded = self
|
||||
.update_editor(|editor, _, cx| editor.is_buffer_folded(snapshot.remote_id(), cx));
|
||||
let (expected_text, expected_selections) =
|
||||
marked_text_ranges(expected_excerpts[ix], true);
|
||||
if expected_text == "[FOLDED]\n" {
|
||||
assert!(is_folded, "excerpt {} should be folded", ix);
|
||||
let is_selected = selections.iter().any(|s| s.head().excerpt_id == excerpt_id);
|
||||
if expected_selections.len() > 0 {
|
||||
assert!(
|
||||
is_selected,
|
||||
"excerpt {} should be selected. Got {:?}",
|
||||
ix,
|
||||
self.editor_state()
|
||||
);
|
||||
} else {
|
||||
assert!(!is_selected, "excerpt {} should not be selected", ix);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
assert!(!is_folded, "excerpt {} should not be folded", ix);
|
||||
assert_eq!(
|
||||
multibuffer_snapshot
|
||||
.text_for_range(Anchor::range_in_buffer(
|
||||
excerpt_id,
|
||||
snapshot.remote_id(),
|
||||
range.context.clone()
|
||||
))
|
||||
.collect::<String>(),
|
||||
expected_text
|
||||
);
|
||||
|
||||
let selections = selections
|
||||
.iter()
|
||||
.filter(|s| s.head().excerpt_id == excerpt_id)
|
||||
.map(|s| {
|
||||
let head = text::ToOffset::to_offset(&s.head().text_anchor, &snapshot)
|
||||
- text::ToOffset::to_offset(&range.context.start, &snapshot);
|
||||
let tail = text::ToOffset::to_offset(&s.head().text_anchor, &snapshot)
|
||||
- text::ToOffset::to_offset(&range.context.start, &snapshot);
|
||||
tail..head
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
// todo: selections that cross excerpt boundaries..
|
||||
assert_eq!(
|
||||
selections, expected_selections,
|
||||
"excerpt {} has incorrect selections",
|
||||
ix,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Make an assertion about the editor's text and the ranges and directions
|
||||
/// of its selections using a string containing embedded range markers.
|
||||
///
|
||||
@@ -392,6 +481,17 @@ impl EditorTestContext {
|
||||
self.assert_selections(expected_selections, marked_text.to_string())
|
||||
}
|
||||
|
||||
/// Make an assertion about the editor's text and the ranges and directions
|
||||
/// of its selections using a string containing embedded range markers.
|
||||
///
|
||||
/// See the `util::test::marked_text_ranges` function for more information.
|
||||
#[track_caller]
|
||||
pub fn assert_display_state(&mut self, marked_text: &str) {
|
||||
let (expected_text, expected_selections) = marked_text_ranges(marked_text, true);
|
||||
pretty_assertions::assert_eq!(self.display_text(), expected_text, "unexpected buffer text");
|
||||
self.assert_selections(expected_selections, marked_text.to_string())
|
||||
}
|
||||
|
||||
pub fn editor_state(&mut self) -> String {
|
||||
generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
|
||||
}
|
||||
|
||||
@@ -522,7 +522,7 @@ impl ExtensionsPage {
|
||||
extension.authors.join(", ")
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.text_ellipsis(),
|
||||
.truncate(),
|
||||
)
|
||||
.child(Label::new("<>").size(LabelSize::Small)),
|
||||
)
|
||||
@@ -534,7 +534,7 @@ impl ExtensionsPage {
|
||||
Label::new(description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Default)
|
||||
.text_ellipsis()
|
||||
.truncate()
|
||||
}))
|
||||
.children(repository_url.map(|repository_url| {
|
||||
IconButton::new(
|
||||
@@ -665,7 +665,7 @@ impl ExtensionsPage {
|
||||
extension.manifest.authors.join(", ")
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.text_ellipsis(),
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
Label::new(format!(
|
||||
@@ -683,7 +683,7 @@ impl ExtensionsPage {
|
||||
Label::new(description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Default)
|
||||
.text_ellipsis()
|
||||
.truncate()
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
|
||||
@@ -135,6 +135,7 @@ pub trait Fs: Send + Sync {
|
||||
Arc<dyn Watcher>,
|
||||
);
|
||||
|
||||
fn home_dir(&self) -> Option<PathBuf>;
|
||||
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>>;
|
||||
fn is_fake(&self) -> bool;
|
||||
async fn is_case_sensitive(&self) -> Result<bool>;
|
||||
@@ -813,6 +814,10 @@ impl Fs for RealFs {
|
||||
temp_dir.close()?;
|
||||
case_sensitive
|
||||
}
|
||||
|
||||
fn home_dir(&self) -> Option<PathBuf> {
|
||||
Some(paths::home_dir().clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
|
||||
@@ -846,6 +851,7 @@ struct FakeFsState {
|
||||
metadata_call_count: usize,
|
||||
read_dir_call_count: usize,
|
||||
moves: std::collections::HashMap<u64, PathBuf>,
|
||||
home_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -1031,6 +1037,7 @@ impl FakeFs {
|
||||
read_dir_call_count: 0,
|
||||
metadata_call_count: 0,
|
||||
moves: Default::default(),
|
||||
home_dir: None,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1441,6 +1448,12 @@ impl FakeFs {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_error_message_for_index_write(&self, dot_git: &Path, message: Option<String>) {
|
||||
self.with_git_state(dot_git, true, |state| {
|
||||
state.simulated_index_write_error_message = message;
|
||||
});
|
||||
}
|
||||
|
||||
pub fn paths(&self, include_dot_git: bool) -> Vec<PathBuf> {
|
||||
let mut result = Vec::new();
|
||||
let mut queue = collections::VecDeque::new();
|
||||
@@ -1524,6 +1537,10 @@ impl FakeFs {
|
||||
fn simulate_random_delay(&self) -> impl futures::Future<Output = ()> {
|
||||
self.executor.simulate_random_delay()
|
||||
}
|
||||
|
||||
pub fn set_home_dir(&self, home_dir: PathBuf) {
|
||||
self.state.lock().home_dir = Some(home_dir);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -2079,6 +2096,10 @@ impl Fs for FakeFs {
|
||||
fn as_fake(&self) -> Arc<FakeFs> {
|
||||
self.this.upgrade().unwrap()
|
||||
}
|
||||
|
||||
fn home_dir(&self) -> Option<PathBuf> {
|
||||
self.state.lock().home_dir.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator<Item = &str> {
|
||||
|
||||
@@ -16,6 +16,7 @@ test-support = []
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
askpass.workspace = true
|
||||
async-trait.workspace = true
|
||||
collections.workspace = true
|
||||
derive_more.workspace = true
|
||||
@@ -34,7 +35,7 @@ text.workspace = true
|
||||
time.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
tempfile.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions.workspace = true
|
||||
|
||||
@@ -8,9 +8,6 @@ pub mod status;
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use gpui::action_with_deprecated_aliases;
|
||||
use gpui::actions;
|
||||
use gpui::impl_actions;
|
||||
use repository::PushOptions;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
@@ -31,28 +28,13 @@ pub static COMMIT_MESSAGE: LazyLock<&'static OsStr> =
|
||||
LazyLock::new(|| OsStr::new("COMMIT_EDITMSG"));
|
||||
pub static INDEX_LOCK: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("index.lock"));
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Deserialize, JsonSchema)]
|
||||
pub struct Push {
|
||||
pub options: Option<PushOptions>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Deserialize, JsonSchema)]
|
||||
pub struct StageAndNext {
|
||||
pub whole_excerpt: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Deserialize, JsonSchema)]
|
||||
pub struct UnstageAndNext {
|
||||
pub whole_excerpt: bool,
|
||||
}
|
||||
|
||||
impl_actions!(git, [Push, StageAndNext, UnstageAndNext]);
|
||||
|
||||
actions!(
|
||||
git,
|
||||
[
|
||||
// per-hunk
|
||||
ToggleStaged,
|
||||
StageAndNext,
|
||||
UnstageAndNext,
|
||||
// per-file
|
||||
StageFile,
|
||||
UnstageFile,
|
||||
@@ -62,10 +44,12 @@ actions!(
|
||||
RestoreTrackedFiles,
|
||||
TrashUntrackedFiles,
|
||||
Uncommit,
|
||||
Push,
|
||||
ForcePush,
|
||||
Pull,
|
||||
Fetch,
|
||||
Commit,
|
||||
ShowCommitEditor,
|
||||
ExpandCommitEditor
|
||||
]
|
||||
);
|
||||
action_with_deprecated_aliases!(git, RestoreFile, ["editor::RevertFile"]);
|
||||
|
||||
@@ -2,7 +2,9 @@ use crate::status::FileStatus;
|
||||
use crate::GitHostingProviderRegistry;
|
||||
use crate::{blame::Blame, status::GitStatus};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use askpass::{AskPassResult, AskPassSession};
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::{select_biased, FutureExt as _};
|
||||
use git2::BranchType;
|
||||
use gpui::SharedString;
|
||||
use parking_lot::Mutex;
|
||||
@@ -11,8 +13,6 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use std::borrow::Borrow;
|
||||
use std::io::Write as _;
|
||||
#[cfg(not(windows))]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::process::Stdio;
|
||||
use std::sync::LazyLock;
|
||||
use std::{
|
||||
@@ -21,9 +21,11 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use sum_tree::MapSeekTarget;
|
||||
use util::command::new_std_command;
|
||||
use util::command::{new_smol_command, new_std_command};
|
||||
use util::ResultExt;
|
||||
|
||||
pub const REMOTE_CANCELLED_BY_USER: &str = "Operation cancelled by user";
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct Branch {
|
||||
pub is_head: bool,
|
||||
@@ -199,10 +201,21 @@ pub trait GitRepository: Send + Sync {
|
||||
branch_name: &str,
|
||||
upstream_name: &str,
|
||||
options: Option<PushOptions>,
|
||||
askpass: AskPassSession,
|
||||
) -> Result<RemoteCommandOutput>;
|
||||
fn pull(&self, branch_name: &str, upstream_name: &str) -> Result<RemoteCommandOutput>;
|
||||
|
||||
fn pull(
|
||||
&self,
|
||||
branch_name: &str,
|
||||
upstream_name: &str,
|
||||
askpass: AskPassSession,
|
||||
) -> Result<RemoteCommandOutput>;
|
||||
fn fetch(&self, askpass: AskPassSession) -> Result<RemoteCommandOutput>;
|
||||
|
||||
fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>>;
|
||||
fn fetch(&self) -> Result<RemoteCommandOutput>;
|
||||
|
||||
/// returns a list of remote branches that contain HEAD
|
||||
fn check_for_pushed_commit(&self) -> Result<Vec<SharedString>>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
|
||||
@@ -427,6 +440,15 @@ impl GitRepository for RealGitRepository {
|
||||
true
|
||||
})
|
||||
.ok();
|
||||
if let Some(oid) = self
|
||||
.repository
|
||||
.lock()
|
||||
.find_reference("CHERRY_PICK_HEAD")
|
||||
.ok()
|
||||
.and_then(|reference| reference.target())
|
||||
{
|
||||
shas.push(oid.to_string())
|
||||
}
|
||||
shas
|
||||
}
|
||||
|
||||
@@ -563,7 +585,6 @@ impl GitRepository for RealGitRepository {
|
||||
.args(paths.iter().map(|p| p.as_ref()))
|
||||
.output()?;
|
||||
|
||||
// TODO: Get remote response out of this and show it to the user
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Failed to stage paths:\n{}",
|
||||
@@ -584,7 +605,6 @@ impl GitRepository for RealGitRepository {
|
||||
.args(paths.iter().map(|p| p.as_ref()))
|
||||
.output()?;
|
||||
|
||||
// TODO: Get remote response out of this and show it to the user
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Failed to unstage:\n{}",
|
||||
@@ -610,7 +630,6 @@ impl GitRepository for RealGitRepository {
|
||||
|
||||
let output = cmd.output()?;
|
||||
|
||||
// TODO: Get remote response out of this and show it to the user
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Failed to commit:\n{}",
|
||||
@@ -625,15 +644,15 @@ impl GitRepository for RealGitRepository {
|
||||
branch_name: &str,
|
||||
remote_name: &str,
|
||||
options: Option<PushOptions>,
|
||||
ask_pass: AskPassSession,
|
||||
) -> Result<RemoteCommandOutput> {
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
// We do this on every operation to ensure that the askpass script exists and is executable.
|
||||
#[cfg(not(windows))]
|
||||
let (askpass_script_path, _temp_dir) = setup_askpass()?;
|
||||
|
||||
let mut command = new_std_command("git");
|
||||
let mut command = new_smol_command("git");
|
||||
command
|
||||
.env("GIT_ASKPASS", ask_pass.script_path())
|
||||
.env("SSH_ASKPASS", ask_pass.script_path())
|
||||
.env("SSH_ASKPASS_REQUIRE", "force")
|
||||
.current_dir(&working_directory)
|
||||
.args(["push"])
|
||||
.args(options.map(|option| match option {
|
||||
@@ -642,91 +661,46 @@ impl GitRepository for RealGitRepository {
|
||||
}))
|
||||
.arg(remote_name)
|
||||
.arg(format!("{}:{}", branch_name, branch_name));
|
||||
let git_process = command.spawn()?;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
command.env("GIT_ASKPASS", askpass_script_path);
|
||||
}
|
||||
|
||||
let output = command.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Failed to push:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
} else {
|
||||
return Ok(RemoteCommandOutput {
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
});
|
||||
}
|
||||
run_remote_command(ask_pass, git_process)
|
||||
}
|
||||
|
||||
fn pull(&self, branch_name: &str, remote_name: &str) -> Result<RemoteCommandOutput> {
|
||||
fn pull(
|
||||
&self,
|
||||
branch_name: &str,
|
||||
remote_name: &str,
|
||||
ask_pass: AskPassSession,
|
||||
) -> Result<RemoteCommandOutput> {
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
// We do this on every operation to ensure that the askpass script exists and is executable.
|
||||
#[cfg(not(windows))]
|
||||
let (askpass_script_path, _temp_dir) = setup_askpass()?;
|
||||
|
||||
let mut command = new_std_command("git");
|
||||
let mut command = new_smol_command("git");
|
||||
command
|
||||
.env("GIT_ASKPASS", ask_pass.script_path())
|
||||
.env("SSH_ASKPASS", ask_pass.script_path())
|
||||
.env("SSH_ASKPASS_REQUIRE", "force")
|
||||
.current_dir(&working_directory)
|
||||
.args(["pull"])
|
||||
.arg(remote_name)
|
||||
.arg(branch_name);
|
||||
let git_process = command.spawn()?;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
command.env("GIT_ASKPASS", askpass_script_path);
|
||||
}
|
||||
|
||||
let output = command.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Failed to pull:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
} else {
|
||||
return Ok(RemoteCommandOutput {
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
});
|
||||
}
|
||||
run_remote_command(ask_pass, git_process)
|
||||
}
|
||||
|
||||
fn fetch(&self) -> Result<RemoteCommandOutput> {
|
||||
fn fetch(&self, ask_pass: AskPassSession) -> Result<RemoteCommandOutput> {
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
// We do this on every operation to ensure that the askpass script exists and is executable.
|
||||
#[cfg(not(windows))]
|
||||
let (askpass_script_path, _temp_dir) = setup_askpass()?;
|
||||
|
||||
let mut command = new_std_command("git");
|
||||
let mut command = new_smol_command("git");
|
||||
command
|
||||
.env("GIT_ASKPASS", ask_pass.script_path())
|
||||
.env("SSH_ASKPASS", ask_pass.script_path())
|
||||
.env("SSH_ASKPASS_REQUIRE", "force")
|
||||
.current_dir(&working_directory)
|
||||
.args(["fetch", "--all"]);
|
||||
let git_process = command.spawn()?;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
command.env("GIT_ASKPASS", askpass_script_path);
|
||||
}
|
||||
|
||||
let output = command.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Failed to fetch:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
} else {
|
||||
return Ok(RemoteCommandOutput {
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
});
|
||||
}
|
||||
run_remote_command(ask_pass, git_process)
|
||||
}
|
||||
|
||||
fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>> {
|
||||
@@ -770,18 +744,88 @@ impl GitRepository for RealGitRepository {
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn check_for_pushed_commit(&self) -> Result<Vec<SharedString>> {
|
||||
let working_directory = self.working_directory()?;
|
||||
let git_cmd = |args: &[&str]| -> Result<String> {
|
||||
let output = new_std_command(&self.git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
.args(args)
|
||||
.output()?;
|
||||
if output.status.success() {
|
||||
Ok(String::from_utf8(output.stdout)?)
|
||||
} else {
|
||||
Err(anyhow!(String::from_utf8_lossy(&output.stderr).to_string()))
|
||||
}
|
||||
};
|
||||
|
||||
let head = git_cmd(&["rev-parse", "HEAD"])
|
||||
.context("Failed to get HEAD")?
|
||||
.trim()
|
||||
.to_owned();
|
||||
|
||||
let mut remote_branches = vec![];
|
||||
let mut add_if_matching = |remote_head: &str| {
|
||||
if let Ok(merge_base) = git_cmd(&["merge-base", &head, remote_head]) {
|
||||
if merge_base.trim() == head {
|
||||
if let Some(s) = remote_head.strip_prefix("refs/remotes/") {
|
||||
remote_branches.push(s.to_owned().into());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// check the main branch of each remote
|
||||
let remotes = git_cmd(&["remote"]).context("Failed to get remotes")?;
|
||||
for remote in remotes.lines() {
|
||||
if let Ok(remote_head) =
|
||||
git_cmd(&["symbolic-ref", &format!("refs/remotes/{remote}/HEAD")])
|
||||
{
|
||||
add_if_matching(remote_head.trim());
|
||||
}
|
||||
}
|
||||
|
||||
// ... and the remote branch that the checked-out one is tracking
|
||||
if let Ok(remote_head) = git_cmd(&["rev-parse", "--symbolic-full-name", "@{u}"]) {
|
||||
add_if_matching(remote_head.trim());
|
||||
}
|
||||
|
||||
Ok(remote_branches)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn setup_askpass() -> Result<(PathBuf, tempfile::TempDir), anyhow::Error> {
|
||||
let temp_dir = tempfile::Builder::new()
|
||||
.prefix("zed-git-askpass")
|
||||
.tempdir()?;
|
||||
let askpass_script = "#!/bin/sh\necho ''";
|
||||
let askpass_script_path = temp_dir.path().join("git-askpass.sh");
|
||||
std::fs::write(&askpass_script_path, askpass_script)?;
|
||||
std::fs::set_permissions(&askpass_script_path, std::fs::Permissions::from_mode(0o755))?;
|
||||
Ok((askpass_script_path, temp_dir))
|
||||
fn run_remote_command(
|
||||
mut ask_pass: AskPassSession,
|
||||
git_process: smol::process::Child,
|
||||
) -> std::result::Result<RemoteCommandOutput, anyhow::Error> {
|
||||
smol::block_on(async {
|
||||
select_biased! {
|
||||
result = ask_pass.run().fuse() => {
|
||||
match result {
|
||||
AskPassResult::CancelledByUser => {
|
||||
Err(anyhow!(REMOTE_CANCELLED_BY_USER))?
|
||||
}
|
||||
AskPassResult::Timedout => {
|
||||
Err(anyhow!("Connecting to host timed out"))?
|
||||
}
|
||||
}
|
||||
}
|
||||
output = git_process.output().fuse() => {
|
||||
let output = output?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!(
|
||||
"Operation failed:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
))
|
||||
} else {
|
||||
Ok(RemoteCommandOutput {
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -799,6 +843,7 @@ pub struct FakeGitRepositoryState {
|
||||
pub statuses: HashMap<RepoPath, FileStatus>,
|
||||
pub current_branch_name: Option<String>,
|
||||
pub branches: HashSet<String>,
|
||||
pub simulated_index_write_error_message: Option<String>,
|
||||
}
|
||||
|
||||
impl FakeGitRepository {
|
||||
@@ -818,6 +863,7 @@ impl FakeGitRepositoryState {
|
||||
statuses: Default::default(),
|
||||
current_branch_name: Default::default(),
|
||||
branches: Default::default(),
|
||||
simulated_index_write_error_message: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -837,6 +883,9 @@ impl GitRepository for FakeGitRepository {
|
||||
|
||||
fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
|
||||
let mut state = self.state.lock();
|
||||
if let Some(message) = state.simulated_index_write_error_message.clone() {
|
||||
return Err(anyhow::anyhow!(message));
|
||||
}
|
||||
if let Some(content) = content {
|
||||
state.index_contents.insert(path.clone(), content);
|
||||
} else {
|
||||
@@ -972,21 +1021,31 @@ impl GitRepository for FakeGitRepository {
|
||||
_branch: &str,
|
||||
_remote: &str,
|
||||
_options: Option<PushOptions>,
|
||||
_ask_pass: AskPassSession,
|
||||
) -> Result<RemoteCommandOutput> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn pull(&self, _branch: &str, _remote: &str) -> Result<RemoteCommandOutput> {
|
||||
fn pull(
|
||||
&self,
|
||||
_branch: &str,
|
||||
_remote: &str,
|
||||
_ask_pass: AskPassSession,
|
||||
) -> Result<RemoteCommandOutput> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn fetch(&self) -> Result<RemoteCommandOutput> {
|
||||
fn fetch(&self, _ask_pass: AskPassSession) -> Result<RemoteCommandOutput> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn get_remotes(&self, _branch: Option<&str>) -> Result<Vec<Remote>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn check_for_pushed_commit(&self) -> Result<Vec<SharedString>> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
|
||||
|
||||
@@ -18,6 +18,7 @@ test-support = ["multi_buffer/test-support"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
askpass.workspace= true
|
||||
buffer_diff.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
@@ -57,6 +58,8 @@ zed_actions.workspace = true
|
||||
windows.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
|
||||
101
crates/git_ui/src/askpass_modal.rs
Normal file
101
crates/git_ui/src/askpass_modal.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use editor::Editor;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Styled};
|
||||
use ui::{
|
||||
div, h_flex, v_flex, ActiveTheme, App, Context, DynamicSpacing, Headline, HeadlineSize, Icon,
|
||||
IconName, IconSize, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||
StyledExt, StyledTypography, Window,
|
||||
};
|
||||
use workspace::ModalView;
|
||||
|
||||
pub(crate) struct AskPassModal {
|
||||
operation: SharedString,
|
||||
prompt: SharedString,
|
||||
editor: Entity<Editor>,
|
||||
tx: Option<oneshot::Sender<String>>,
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for AskPassModal {}
|
||||
impl ModalView for AskPassModal {}
|
||||
impl Focusable for AskPassModal {
|
||||
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
|
||||
self.editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl AskPassModal {
|
||||
pub fn new(
|
||||
operation: SharedString,
|
||||
prompt: SharedString,
|
||||
tx: oneshot::Sender<String>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::single_line(window, cx);
|
||||
if prompt.contains("yes/no") {
|
||||
editor.set_masked(false, cx);
|
||||
} else {
|
||||
editor.set_masked(true, cx);
|
||||
}
|
||||
editor
|
||||
});
|
||||
Self {
|
||||
operation,
|
||||
prompt,
|
||||
editor,
|
||||
tx: Some(tx),
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(tx) = self.tx.take() {
|
||||
tx.send(self.editor.read(cx).text(cx)).ok();
|
||||
}
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AskPassModal {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("PasswordPrompt")
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.elevation_2(cx)
|
||||
.size_full()
|
||||
.font_buffer(cx)
|
||||
.child(
|
||||
h_flex()
|
||||
.px(DynamicSpacing::Base12.rems(cx))
|
||||
.pt(DynamicSpacing::Base08.rems(cx))
|
||||
.pb(DynamicSpacing::Base04.rems(cx))
|
||||
.rounded_t_md()
|
||||
.w_full()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(IconName::GitBranch).size(IconSize::XSmall))
|
||||
.child(h_flex().gap_1().overflow_x_hidden().child(
|
||||
div().max_w_96().overflow_x_hidden().text_ellipsis().child(
|
||||
Headline::new(self.operation.clone()).size(HeadlineSize::XSmall),
|
||||
),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_buffer(cx)
|
||||
.py_2()
|
||||
.px_3()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.child(self.prompt.clone())
|
||||
.child(self.editor.clone()),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
|
||||
use git::repository::Branch;
|
||||
use gpui::{
|
||||
rems, App, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
rems, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||
Task, Window,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{Project, ProjectPath};
|
||||
use project::git::Repository;
|
||||
use std::sync::Arc;
|
||||
use ui::{
|
||||
prelude::*, HighlightedLabel, ListItem, ListItemSpacing, PopoverMenuHandle, TriggerablePopover,
|
||||
};
|
||||
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing, PopoverMenuHandle};
|
||||
use util::ResultExt;
|
||||
use workspace::notifications::DetachAndPromptErr;
|
||||
use workspace::{ModalView, Workspace};
|
||||
@@ -30,36 +28,21 @@ pub fn open(
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let project = workspace.project().clone();
|
||||
let this = cx.entity();
|
||||
let repository = workspace.project().read(cx).active_repository(cx).clone();
|
||||
let style = BranchListStyle::Modal;
|
||||
cx.spawn_in(window, |_, mut cx| async move {
|
||||
// Modal branch picker has a longer trailoff than a popover one.
|
||||
let delegate = BranchListDelegate::new(project.clone(), style, 70, &cx).await?;
|
||||
|
||||
this.update_in(&mut cx, move |workspace, window, cx| {
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
});
|
||||
|
||||
let mut list = BranchList::new(project, style, 34., cx);
|
||||
list._subscription = Some(_subscription);
|
||||
list.picker = Some(picker);
|
||||
list
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
BranchList::new(repository, style, 34., window, cx)
|
||||
})
|
||||
.detach_and_prompt_err("Failed to read branches", window, cx, |_, _, _| None)
|
||||
}
|
||||
|
||||
pub fn popover(project: Entity<Project>, window: &mut Window, cx: &mut App) -> Entity<BranchList> {
|
||||
pub fn popover(
|
||||
repository: Option<Entity<Repository>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<BranchList> {
|
||||
cx.new(|cx| {
|
||||
let mut list = BranchList::new(project, BranchListStyle::Popover, 15., cx);
|
||||
list.reload_branches(window, cx);
|
||||
let list = BranchList::new(repository, BranchListStyle::Popover, 15., window, cx);
|
||||
list.focus_handle(cx).focus(window);
|
||||
list
|
||||
})
|
||||
}
|
||||
@@ -72,59 +55,53 @@ enum BranchListStyle {
|
||||
|
||||
pub struct BranchList {
|
||||
rem_width: f32,
|
||||
popover_handle: PopoverMenuHandle<Self>,
|
||||
default_focus_handle: FocusHandle,
|
||||
project: Entity<Project>,
|
||||
style: BranchListStyle,
|
||||
pub picker: Option<Entity<Picker<BranchListDelegate>>>,
|
||||
_subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl TriggerablePopover for BranchList {
|
||||
fn menu_handle(
|
||||
&mut self,
|
||||
_window: &mut Window,
|
||||
_cx: &mut gpui::Context<Self>,
|
||||
) -> PopoverMenuHandle<Self> {
|
||||
self.popover_handle.clone()
|
||||
}
|
||||
pub popover_handle: PopoverMenuHandle<Self>,
|
||||
pub picker: Entity<Picker<BranchListDelegate>>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl BranchList {
|
||||
fn new(project: Entity<Project>, style: BranchListStyle, rem_width: f32, cx: &mut App) -> Self {
|
||||
fn new(
|
||||
repository: Option<Entity<Repository>>,
|
||||
style: BranchListStyle,
|
||||
rem_width: f32,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let popover_handle = PopoverMenuHandle::default();
|
||||
Self {
|
||||
project,
|
||||
picker: None,
|
||||
rem_width,
|
||||
popover_handle,
|
||||
default_focus_handle: cx.focus_handle(),
|
||||
style,
|
||||
_subscription: None,
|
||||
}
|
||||
}
|
||||
let all_branches_request = repository
|
||||
.clone()
|
||||
.map(|repository| repository.read(cx).branches());
|
||||
|
||||
fn reload_branches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let project = self.project.clone();
|
||||
let style = self.style;
|
||||
cx.spawn_in(window, |this, mut cx| async move {
|
||||
let delegate = BranchListDelegate::new(project, style, 20, &cx).await?;
|
||||
let picker =
|
||||
cx.new_window_entity(|window, cx| Picker::uniform_list(delegate, window, cx))?;
|
||||
let all_branches = all_branches_request
|
||||
.context("No active repository")?
|
||||
.await??;
|
||||
|
||||
this.update(&mut cx, |branch_list, cx| {
|
||||
let subscription =
|
||||
cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| cx.emit(DismissEvent));
|
||||
|
||||
branch_list.picker = Some(picker);
|
||||
branch_list._subscription = Some(subscription);
|
||||
|
||||
cx.notify();
|
||||
this.update_in(&mut cx, |this, window, cx| {
|
||||
this.picker.update(cx, |picker, cx| {
|
||||
picker.delegate.all_branches = Some(all_branches);
|
||||
picker.refresh(window, cx);
|
||||
})
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
let delegate = BranchListDelegate::new(repository.clone(), style, 20);
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
|
||||
let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
});
|
||||
|
||||
Self {
|
||||
picker,
|
||||
rem_width,
|
||||
popover_handle,
|
||||
_subscription,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ModalView for BranchList {}
|
||||
@@ -132,10 +109,7 @@ impl EventEmitter<DismissEvent> for BranchList {}
|
||||
|
||||
impl Focusable for BranchList {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.picker
|
||||
.as_ref()
|
||||
.map(|picker| picker.focus_handle(cx))
|
||||
.unwrap_or_else(|| self.default_focus_handle.clone())
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,24 +117,13 @@ impl Render for BranchList {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.w(rems(self.rem_width))
|
||||
.map(|parent| match self.picker.as_ref() {
|
||||
Some(picker) => parent.child(picker.clone()).on_mouse_down_out({
|
||||
let picker = picker.clone();
|
||||
cx.listener(move |_, _, window, cx| {
|
||||
picker.update(cx, |this, cx| {
|
||||
this.cancel(&Default::default(), window, cx);
|
||||
})
|
||||
.child(self.picker.clone())
|
||||
.on_mouse_down_out({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.picker.update(cx, |this, cx| {
|
||||
this.cancel(&Default::default(), window, cx);
|
||||
})
|
||||
}),
|
||||
None => parent.child(
|
||||
h_flex()
|
||||
.id("branch-picker-error")
|
||||
.on_click(
|
||||
cx.listener(|this, _, window, cx| this.reload_branches(window, cx)),
|
||||
)
|
||||
.child("Could not load branches.")
|
||||
.child("Click to retry"),
|
||||
),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -184,8 +147,8 @@ impl BranchEntry {
|
||||
|
||||
pub struct BranchListDelegate {
|
||||
matches: Vec<BranchEntry>,
|
||||
all_branches: Vec<Branch>,
|
||||
project: Entity<Project>,
|
||||
all_branches: Option<Vec<Branch>>,
|
||||
repo: Option<Entity<Repository>>,
|
||||
style: BranchListStyle,
|
||||
selected_index: usize,
|
||||
last_query: String,
|
||||
@@ -194,33 +157,20 @@ pub struct BranchListDelegate {
|
||||
}
|
||||
|
||||
impl BranchListDelegate {
|
||||
async fn new(
|
||||
project: Entity<Project>,
|
||||
fn new(
|
||||
repo: Option<Entity<Repository>>,
|
||||
style: BranchListStyle,
|
||||
branch_name_trailoff_after: usize,
|
||||
cx: &AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let all_branches_request = cx.update(|cx| {
|
||||
let project = project.read(cx);
|
||||
let first_worktree = project
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.context("No worktrees found")?;
|
||||
let project_path = ProjectPath::root_path(first_worktree.read(cx).id());
|
||||
anyhow::Ok(project.branches(project_path, cx))
|
||||
})??;
|
||||
|
||||
let all_branches = all_branches_request.await?;
|
||||
|
||||
Ok(Self {
|
||||
) -> Self {
|
||||
Self {
|
||||
matches: vec![],
|
||||
project,
|
||||
repo,
|
||||
style,
|
||||
all_branches,
|
||||
all_branches: None,
|
||||
selected_index: 0,
|
||||
last_query: Default::default(),
|
||||
branch_name_trailoff_after,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn branch_count(&self) -> usize {
|
||||
@@ -261,32 +211,31 @@ impl PickerDelegate for BranchListDelegate {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let Some(mut all_branches) = self.all_branches.clone() else {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
cx.spawn_in(window, move |picker, mut cx| async move {
|
||||
let candidates = picker.update(&mut cx, |picker, _| {
|
||||
const RECENT_BRANCHES_COUNT: usize = 10;
|
||||
let mut branches = picker.delegate.all_branches.clone();
|
||||
if query.is_empty() {
|
||||
if branches.len() > RECENT_BRANCHES_COUNT {
|
||||
// Truncate list of recent branches
|
||||
// Do a partial sort to show recent-ish branches first.
|
||||
branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
|
||||
rhs.priority_key().cmp(&lhs.priority_key())
|
||||
});
|
||||
branches.truncate(RECENT_BRANCHES_COUNT);
|
||||
}
|
||||
branches.sort_unstable_by(|lhs, rhs| {
|
||||
rhs.is_head.cmp(&lhs.is_head).then(lhs.name.cmp(&rhs.name))
|
||||
const RECENT_BRANCHES_COUNT: usize = 10;
|
||||
if query.is_empty() {
|
||||
if all_branches.len() > RECENT_BRANCHES_COUNT {
|
||||
// Truncate list of recent branches
|
||||
// Do a partial sort to show recent-ish branches first.
|
||||
all_branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
|
||||
rhs.priority_key().cmp(&lhs.priority_key())
|
||||
});
|
||||
all_branches.truncate(RECENT_BRANCHES_COUNT);
|
||||
}
|
||||
branches
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name))
|
||||
.collect::<Vec<StringMatchCandidate>>()
|
||||
});
|
||||
let Some(candidates) = candidates.log_err() else {
|
||||
return;
|
||||
};
|
||||
all_branches.sort_unstable_by(|lhs, rhs| {
|
||||
rhs.is_head.cmp(&lhs.is_head).then(lhs.name.cmp(&rhs.name))
|
||||
});
|
||||
}
|
||||
|
||||
let candidates = all_branches
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name))
|
||||
.collect::<Vec<StringMatchCandidate>>();
|
||||
let matches: Vec<BranchEntry> = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
@@ -334,14 +283,16 @@ impl PickerDelegate for BranchListDelegate {
|
||||
return;
|
||||
};
|
||||
|
||||
let current_branch = self.project.update(cx, |project, cx| {
|
||||
project
|
||||
.active_repository(cx)
|
||||
.and_then(|repo| repo.read(cx).current_branch())
|
||||
.map(|branch| branch.name.to_string())
|
||||
let current_branch = self.repo.as_ref().map(|repo| {
|
||||
repo.update(cx, |repo, _| {
|
||||
repo.current_branch().map(|branch| branch.name.clone())
|
||||
})
|
||||
});
|
||||
|
||||
if current_branch == Some(branch.name().to_string()) {
|
||||
if current_branch
|
||||
.flatten()
|
||||
.is_some_and(|current_branch| current_branch == branch.name())
|
||||
{
|
||||
cx.emit(DismissEvent);
|
||||
return;
|
||||
}
|
||||
@@ -350,19 +301,33 @@ impl PickerDelegate for BranchListDelegate {
|
||||
let branch = branch.clone();
|
||||
|picker, mut cx| async move {
|
||||
let branch_change_task = picker.update(&mut cx, |this, cx| {
|
||||
let project = this.delegate.project.read(cx);
|
||||
let branch_to_checkout = match branch {
|
||||
BranchEntry::Branch(branch) => branch.string,
|
||||
BranchEntry::History(string) => string,
|
||||
BranchEntry::NewBranch { name: branch_name } => branch_name,
|
||||
};
|
||||
let worktree = project
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.context("worktree disappeared")?;
|
||||
let repository = ProjectPath::root_path(worktree.read(cx).id());
|
||||
let repo = this
|
||||
.delegate
|
||||
.repo
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("No active repository"))?
|
||||
.clone();
|
||||
|
||||
anyhow::Ok(project.update_or_create_branch(repository, branch_to_checkout, cx))
|
||||
let cx = cx.to_async();
|
||||
|
||||
anyhow::Ok(async move {
|
||||
match branch {
|
||||
BranchEntry::Branch(StringMatch {
|
||||
string: branch_name,
|
||||
..
|
||||
})
|
||||
| BranchEntry::History(branch_name) => {
|
||||
cx.update(|cx| repo.read(cx).change_branch(branch_name))?
|
||||
.await?
|
||||
}
|
||||
BranchEntry::NewBranch { name: branch_name } => {
|
||||
cx.update(|cx| repo.read(cx).create_branch(branch_name.clone()))?
|
||||
.await??;
|
||||
cx.update(|cx| repo.read(cx).change_branch(branch_name))?
|
||||
.await?
|
||||
}
|
||||
}
|
||||
})
|
||||
})??;
|
||||
|
||||
branch_change_task.await?;
|
||||
@@ -370,7 +335,7 @@ impl PickerDelegate for BranchListDelegate {
|
||||
picker.update(&mut cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
|
||||
Ok::<(), anyhow::Error>(())
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
use crate::branch_picker::{self, BranchList};
|
||||
use crate::git_panel::{commit_message_editor, GitPanel};
|
||||
use git::{Commit, ShowCommitEditor};
|
||||
use git::Commit;
|
||||
use panel::{panel_button, panel_editor_style, panel_filled_button};
|
||||
use project::Project;
|
||||
use ui::{prelude::*, KeybindingHint, PopoverButton, Tooltip, TriggerablePopover};
|
||||
use ui::{prelude::*, KeybindingHint, PopoverMenu, Tooltip};
|
||||
|
||||
use editor::{Editor, EditorElement};
|
||||
use gpui::*;
|
||||
@@ -110,83 +109,68 @@ struct RestoreDock {
|
||||
|
||||
impl CommitModal {
|
||||
pub fn register(workspace: &mut Workspace, _: &mut Window, _cx: &mut Context<Workspace>) {
|
||||
workspace.register_action(|workspace, _: &ShowCommitEditor, window, cx| {
|
||||
let Some(git_panel) = workspace.panel::<GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let (can_commit, conflict) = git_panel.update(cx, |git_panel, cx| {
|
||||
let can_commit = git_panel.can_commit();
|
||||
let conflict = git_panel.has_unstaged_conflicts();
|
||||
if can_commit {
|
||||
git_panel.set_modal_open(true, cx);
|
||||
}
|
||||
(can_commit, conflict)
|
||||
});
|
||||
if !can_commit {
|
||||
let message = if conflict {
|
||||
"There are still conflicts. You must stage these before committing."
|
||||
} else {
|
||||
"No changes to commit."
|
||||
};
|
||||
let prompt = window.prompt(PromptLevel::Warning, message, None, &["Ok"], cx);
|
||||
cx.spawn(|_, _| async move {
|
||||
prompt.await.ok();
|
||||
})
|
||||
.detach();
|
||||
return;
|
||||
}
|
||||
|
||||
let dock = workspace.dock_at_position(git_panel.position(window, cx));
|
||||
let is_open = dock.read(cx).is_open();
|
||||
let active_index = dock.read(cx).active_panel_index();
|
||||
let dock = dock.downgrade();
|
||||
let restore_dock_position = RestoreDock {
|
||||
dock,
|
||||
is_open,
|
||||
active_index,
|
||||
};
|
||||
|
||||
let project = workspace.project().clone();
|
||||
workspace.open_panel::<GitPanel>(window, cx);
|
||||
workspace.toggle_modal(window, cx, move |window, cx| {
|
||||
CommitModal::new(git_panel, restore_dock_position, project, window, cx)
|
||||
})
|
||||
workspace.register_action(|workspace, _: &Commit, window, cx| {
|
||||
CommitModal::toggle(workspace, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<'_, Workspace>) {
|
||||
let Some(git_panel) = workspace.panel::<GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
git_panel.update(cx, |git_panel, cx| {
|
||||
git_panel.set_modal_open(true, cx);
|
||||
});
|
||||
|
||||
let dock = workspace.dock_at_position(git_panel.position(window, cx));
|
||||
let is_open = dock.read(cx).is_open();
|
||||
let active_index = dock.read(cx).active_panel_index();
|
||||
let dock = dock.downgrade();
|
||||
let restore_dock_position = RestoreDock {
|
||||
dock,
|
||||
is_open,
|
||||
active_index,
|
||||
};
|
||||
|
||||
workspace.open_panel::<GitPanel>(window, cx);
|
||||
workspace.toggle_modal(window, cx, move |window, cx| {
|
||||
CommitModal::new(git_panel, restore_dock_position, window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn new(
|
||||
git_panel: Entity<GitPanel>,
|
||||
restore_dock: RestoreDock,
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let panel = git_panel.read(cx);
|
||||
let suggested_message = panel.suggest_commit_message();
|
||||
let active_repository = panel.active_repository.clone();
|
||||
let suggested_commit_message = panel.suggest_commit_message();
|
||||
|
||||
let commit_editor = git_panel.update(cx, |git_panel, cx| {
|
||||
git_panel.set_modal_open(true, cx);
|
||||
let buffer = git_panel.commit_message_buffer(cx).clone();
|
||||
let panel_editor = git_panel.commit_editor.clone();
|
||||
let project = git_panel.project.clone();
|
||||
cx.new(|cx| commit_message_editor(buffer, None, project.clone(), false, window, cx))
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut editor =
|
||||
commit_message_editor(buffer, None, project.clone(), false, window, cx);
|
||||
editor.sync_selections(panel_editor, cx).detach();
|
||||
|
||||
editor
|
||||
})
|
||||
});
|
||||
|
||||
let commit_message = commit_editor.read(cx).text(cx);
|
||||
|
||||
if let Some(suggested_message) = suggested_message {
|
||||
if let Some(suggested_commit_message) = suggested_commit_message {
|
||||
if commit_message.is_empty() {
|
||||
commit_editor.update(cx, |editor, cx| {
|
||||
editor.set_text(suggested_message, window, cx);
|
||||
editor.select_all(&Default::default(), window, cx);
|
||||
editor.set_placeholder_text(suggested_commit_message, cx);
|
||||
});
|
||||
} else {
|
||||
if commit_message.as_str().trim() == suggested_message.trim() {
|
||||
commit_editor.update(cx, |editor, cx| {
|
||||
// select the message to make it easy to delete
|
||||
editor.select_all(&Default::default(), window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +190,7 @@ impl CommitModal {
|
||||
let properties = ModalContainerProperties::new(window, 50);
|
||||
|
||||
Self {
|
||||
branch_list: branch_picker::popover(project.clone(), window, cx),
|
||||
branch_list: branch_picker::popover(active_repository.clone(), window, cx),
|
||||
git_panel,
|
||||
commit_editor,
|
||||
restore_dock,
|
||||
@@ -250,7 +234,7 @@ impl CommitModal {
|
||||
pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let git_panel = self.git_panel.clone();
|
||||
|
||||
let (branch, tooltip, commit_label, co_authors) =
|
||||
let (branch, can_commit, tooltip, commit_label, co_authors) =
|
||||
self.git_panel.update(cx, |git_panel, cx| {
|
||||
let branch = git_panel
|
||||
.active_repository
|
||||
@@ -262,18 +246,10 @@ impl CommitModal {
|
||||
.map(|b| b.name.clone())
|
||||
})
|
||||
.unwrap_or_else(|| "<no branch>".into());
|
||||
let tooltip = if git_panel.has_staged_changes() {
|
||||
"Commit staged changes"
|
||||
} else {
|
||||
"Commit changes to tracked files"
|
||||
};
|
||||
let title = if git_panel.has_staged_changes() {
|
||||
"Commit"
|
||||
} else {
|
||||
"Commit All"
|
||||
};
|
||||
let (can_commit, tooltip) = git_panel.configure_commit_button(cx);
|
||||
let title = git_panel.commit_button_title();
|
||||
let co_authors = git_panel.render_co_authors(cx);
|
||||
(branch, tooltip, title, co_authors)
|
||||
(branch, can_commit, tooltip, title, co_authors)
|
||||
});
|
||||
|
||||
let branch_picker_button = panel_button(branch)
|
||||
@@ -291,12 +267,20 @@ impl CommitModal {
|
||||
}))
|
||||
.style(ButtonStyle::Transparent);
|
||||
|
||||
let branch_picker = PopoverButton::new(
|
||||
self.branch_list.clone(),
|
||||
Corner::BottomLeft,
|
||||
branch_picker_button,
|
||||
Tooltip::for_action_title("Switch Branch", &zed_actions::git::Branch),
|
||||
);
|
||||
let branch_picker = PopoverMenu::new("popover-button")
|
||||
.menu({
|
||||
let branch_list = self.branch_list.clone();
|
||||
move |_window, _cx| Some(branch_list.clone())
|
||||
})
|
||||
.trigger_with_tooltip(
|
||||
branch_picker_button,
|
||||
Tooltip::for_action_title("Switch Branch", &zed_actions::git::Branch),
|
||||
)
|
||||
.anchor(Corner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-2.0),
|
||||
});
|
||||
|
||||
let close_kb_hint =
|
||||
if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
|
||||
@@ -308,9 +292,8 @@ impl CommitModal {
|
||||
None
|
||||
};
|
||||
|
||||
let (panel_editor_focus_handle, can_commit) = git_panel.update(cx, |git_panel, cx| {
|
||||
(git_panel.editor_focus_handle(cx), git_panel.can_commit())
|
||||
});
|
||||
let panel_editor_focus_handle =
|
||||
git_panel.update(cx, |git_panel, cx| git_panel.editor_focus_handle(cx));
|
||||
|
||||
let commit_button = panel_filled_button(commit_label)
|
||||
.tooltip(move |window, cx| {
|
||||
@@ -332,12 +315,7 @@ impl CommitModal {
|
||||
.w_full()
|
||||
.h(px(self.properties.footer_height))
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(branch_picker.render(window, cx))
|
||||
.children(co_authors),
|
||||
)
|
||||
.child(h_flex().gap_1().child(branch_picker).children(co_authors))
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -354,6 +332,7 @@ impl CommitModal {
|
||||
fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn commit(&mut self, _: &git::Commit, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.git_panel
|
||||
.update(cx, |git_panel, cx| git_panel.commit_changes(window, cx));
|
||||
@@ -377,7 +356,7 @@ impl Render for CommitModal {
|
||||
.on_action(
|
||||
cx.listener(|this, _: &zed_actions::git::Branch, window, cx| {
|
||||
this.branch_list.update(cx, |branch_list, cx| {
|
||||
branch_list.menu_handle(window, cx).toggle(window, cx);
|
||||
branch_list.popover_handle.toggle(window, cx);
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,9 @@ use git_panel_settings::GitPanelSettings;
|
||||
use gpui::App;
|
||||
use project_diff::ProjectDiff;
|
||||
use ui::{ActiveTheme, Color, Icon, IconName, IntoElement};
|
||||
use workspace::Workspace;
|
||||
|
||||
mod askpass_modal;
|
||||
pub mod branch_picker;
|
||||
mod commit_modal;
|
||||
pub mod git_panel;
|
||||
@@ -19,6 +21,47 @@ pub fn init(cx: &mut App) {
|
||||
branch_picker::init(cx);
|
||||
cx.observe_new(ProjectDiff::register).detach();
|
||||
commit_modal::init(cx);
|
||||
git_panel::init(cx);
|
||||
|
||||
cx.observe_new(|workspace: &mut Workspace, _, cx| {
|
||||
let project = workspace.project().read(cx);
|
||||
if project.is_via_collab() {
|
||||
return;
|
||||
}
|
||||
workspace.register_action(|workspace, _: &git::Fetch, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.fetch(window, cx);
|
||||
});
|
||||
});
|
||||
workspace.register_action(|workspace, _: &git::Push, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.push(false, window, cx);
|
||||
});
|
||||
});
|
||||
workspace.register_action(|workspace, _: &git::ForcePush, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.push(true, window, cx);
|
||||
});
|
||||
});
|
||||
workspace.register_action(|workspace, _: &git::Pull, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.pull(window, cx);
|
||||
});
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
// TODO: Add updated status colors to theme
|
||||
|
||||
@@ -5,13 +5,12 @@ use collections::HashSet;
|
||||
use editor::{
|
||||
actions::{GoToHunk, GoToPreviousHunk},
|
||||
scroll::Autoscroll,
|
||||
Editor, EditorEvent, ToPoint,
|
||||
Editor, EditorEvent,
|
||||
};
|
||||
use feature_flags::FeatureFlagViewExt;
|
||||
use futures::StreamExt;
|
||||
use git::{
|
||||
status::FileStatus, ShowCommitEditor, StageAll, StageAndNext, ToggleStaged, UnstageAll,
|
||||
UnstageAndNext,
|
||||
status::FileStatus, Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext,
|
||||
};
|
||||
use gpui::{
|
||||
actions, Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity,
|
||||
@@ -19,7 +18,10 @@ use gpui::{
|
||||
};
|
||||
use language::{Anchor, Buffer, Capability, OffsetRangeExt};
|
||||
use multi_buffer::{MultiBuffer, PathKey};
|
||||
use project::{git::GitStore, Project, ProjectPath};
|
||||
use project::{
|
||||
git::{GitEvent, GitStore},
|
||||
Project, ProjectPath,
|
||||
};
|
||||
use std::any::{Any, TypeId};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{prelude::*, vertical_divider, Tooltip};
|
||||
@@ -141,8 +143,13 @@ impl ProjectDiff {
|
||||
let git_store_subscription = cx.subscribe_in(
|
||||
&git_store,
|
||||
window,
|
||||
move |this, _git_store, _event, _window, _cx| {
|
||||
*this.update_needed.borrow_mut() = ();
|
||||
move |this, _git_store, event, _window, _cx| match event {
|
||||
GitEvent::ActiveRepositoryChanged
|
||||
| GitEvent::FileSystemUpdated
|
||||
| GitEvent::GitStateUpdated => {
|
||||
*this.update_needed.borrow_mut() = ();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -192,6 +199,19 @@ impl ProjectDiff {
|
||||
self.move_to_path(path_key, window, cx)
|
||||
}
|
||||
|
||||
pub fn active_path(&self, cx: &App) -> Option<ProjectPath> {
|
||||
let editor = self.editor.read(cx);
|
||||
let position = editor.selections.newest_anchor().head();
|
||||
let multi_buffer = editor.buffer().read(cx);
|
||||
let (_, buffer, _) = multi_buffer.excerpt_containing(position, cx)?;
|
||||
|
||||
let file = buffer.read(cx).file()?;
|
||||
Some(ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
path: file.path().clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn move_to_path(&mut self, path_key: PathKey, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
@@ -244,14 +264,12 @@ impl ProjectDiff {
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut commit = false;
|
||||
let mut stage_all = false;
|
||||
let mut unstage_all = false;
|
||||
self.workspace
|
||||
.read_with(cx, |workspace, cx| {
|
||||
if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {
|
||||
let git_panel = git_panel.read(cx);
|
||||
commit = git_panel.can_commit();
|
||||
stage_all = git_panel.can_stage_all();
|
||||
unstage_all = git_panel.can_unstage_all();
|
||||
}
|
||||
@@ -263,7 +281,6 @@ impl ProjectDiff {
|
||||
unstage: has_staged_hunks,
|
||||
prev_next,
|
||||
selection,
|
||||
commit,
|
||||
stage_all,
|
||||
unstage_all,
|
||||
};
|
||||
@@ -271,41 +288,26 @@ impl ProjectDiff {
|
||||
|
||||
fn handle_editor_event(
|
||||
&mut self,
|
||||
editor: &Entity<Editor>,
|
||||
_: &Entity<Editor>,
|
||||
event: &EditorEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
EditorEvent::ScrollPositionChanged { .. } => editor.update(cx, |editor, cx| {
|
||||
let anchor = editor.scroll_manager.anchor().anchor;
|
||||
let multibuffer = self.multibuffer.read(cx);
|
||||
let snapshot = multibuffer.snapshot(cx);
|
||||
let mut point = anchor.to_point(&snapshot);
|
||||
point.row = (point.row + 1).min(snapshot.max_row().0);
|
||||
point.column = 0;
|
||||
|
||||
let Some((_, buffer, _)) = self.multibuffer.read(cx).excerpt_containing(point, cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(project_path) = buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map(|file| (file.worktree_id(cx), file.path().clone()))
|
||||
else {
|
||||
EditorEvent::SelectionsChanged { local: true } => {
|
||||
let Some(project_path) = self.active_path(cx) else {
|
||||
return;
|
||||
};
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {
|
||||
git_panel.update(cx, |git_panel, cx| {
|
||||
git_panel.select_entry_by_path(project_path.into(), window, cx)
|
||||
git_panel.select_entry_by_path(project_path, window, cx)
|
||||
})
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}),
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -400,6 +402,7 @@ impl ProjectDiff {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
if was_empty {
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
// TODO select the very beginning (possibly inside a deletion)
|
||||
selections.select_ranges([0..0])
|
||||
});
|
||||
}
|
||||
@@ -774,7 +777,6 @@ struct ButtonStates {
|
||||
selection: bool,
|
||||
stage_all: bool,
|
||||
unstage_all: bool,
|
||||
commit: bool,
|
||||
}
|
||||
|
||||
impl Render for ProjectDiffToolbar {
|
||||
@@ -813,10 +815,8 @@ impl Render for ProjectDiffToolbar {
|
||||
el.child(
|
||||
Button::new("stage", "Stage")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Stage",
|
||||
&StageAndNext {
|
||||
whole_excerpt: false,
|
||||
},
|
||||
"Stage and go to next hunk",
|
||||
&StageAndNext,
|
||||
&focus_handle,
|
||||
))
|
||||
// don't actually disable the button so it's mashable
|
||||
@@ -826,22 +826,14 @@ impl Render for ProjectDiffToolbar {
|
||||
Color::Disabled
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(
|
||||
&StageAndNext {
|
||||
whole_excerpt: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
this.dispatch_action(&StageAndNext, window, cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("unstage", "Unstage")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Unstage",
|
||||
&UnstageAndNext {
|
||||
whole_excerpt: false,
|
||||
},
|
||||
"Unstage and go to next hunk",
|
||||
&UnstageAndNext,
|
||||
&focus_handle,
|
||||
))
|
||||
.color(if button_states.unstage {
|
||||
@@ -850,13 +842,7 @@ impl Render for ProjectDiffToolbar {
|
||||
Color::Disabled
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(
|
||||
&UnstageAndNext {
|
||||
whole_excerpt: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
this.dispatch_action(&UnstageAndNext, window, cx)
|
||||
})),
|
||||
)
|
||||
}),
|
||||
@@ -870,20 +856,12 @@ impl Render for ProjectDiffToolbar {
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Go to previous hunk",
|
||||
&GoToPreviousHunk {
|
||||
center_cursor: false,
|
||||
},
|
||||
&GoToPreviousHunk,
|
||||
&focus_handle,
|
||||
))
|
||||
.disabled(!button_states.prev_next)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(
|
||||
&GoToPreviousHunk {
|
||||
center_cursor: true,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
this.dispatch_action(&GoToPreviousHunk, window, cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
@@ -891,20 +869,12 @@ impl Render for ProjectDiffToolbar {
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Go to next hunk",
|
||||
&GoToHunk {
|
||||
center_cursor: false,
|
||||
},
|
||||
&GoToHunk,
|
||||
&focus_handle,
|
||||
))
|
||||
.disabled(!button_states.prev_next)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(
|
||||
&GoToHunk {
|
||||
center_cursor: true,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
this.dispatch_action(&GoToHunk, window, cx)
|
||||
})),
|
||||
),
|
||||
)
|
||||
@@ -950,26 +920,27 @@ impl Render for ProjectDiffToolbar {
|
||||
)
|
||||
.child(
|
||||
Button::new("commit", "Commit")
|
||||
.disabled(!button_states.commit)
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Commit",
|
||||
&ShowCommitEditor,
|
||||
&Commit,
|
||||
&focus_handle,
|
||||
))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(&ShowCommitEditor, window, cx);
|
||||
this.dispatch_action(&Commit, window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use collections::HashMap;
|
||||
use editor::test::editor_test_context::assert_state_with_diff;
|
||||
use db::indoc;
|
||||
use editor::test::editor_test_context::{assert_state_with_diff, EditorTestContext};
|
||||
use git::status::{StatusCode, TrackedStatus};
|
||||
use gpui::TestAppContext;
|
||||
use project::FakeFs;
|
||||
@@ -980,6 +951,11 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
env_logger::init();
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let store = SettingsStore::test(cx);
|
||||
@@ -1048,9 +1024,6 @@ mod tests {
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.git_restore(&Default::default(), window, cx);
|
||||
});
|
||||
fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
|
||||
state.statuses = HashMap::default();
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_state_with_diff(&editor, cx, &"ˇ".unindent());
|
||||
@@ -1152,4 +1125,196 @@ mod tests {
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_hunks_after_restore_then_modify(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
".git": {},
|
||||
"foo": "modified\n",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(path!("/project/foo"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let buffer_editor = cx.new_window_entity(|window, cx| {
|
||||
Editor::for_buffer(buffer, Some(project.clone()), window, cx)
|
||||
});
|
||||
let diff = cx.new_window_entity(|window, cx| {
|
||||
ProjectDiff::new(project.clone(), workspace, window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
fs.set_head_for_repo(
|
||||
path!("/project/.git").as_ref(),
|
||||
&[("foo".into(), "original\n".into())],
|
||||
);
|
||||
fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
|
||||
state.statuses = HashMap::from_iter([(
|
||||
"foo".into(),
|
||||
TrackedStatus {
|
||||
index_status: StatusCode::Unmodified,
|
||||
worktree_status: StatusCode::Modified,
|
||||
}
|
||||
.into(),
|
||||
)]);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
let diff_editor = diff.update(cx, |diff, _| diff.editor.clone());
|
||||
|
||||
assert_state_with_diff(
|
||||
&diff_editor,
|
||||
cx,
|
||||
&"
|
||||
- original
|
||||
+ ˇmodified
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
let prev_buffer_hunks =
|
||||
cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| {
|
||||
let snapshot = buffer_editor.snapshot(window, cx);
|
||||
let snapshot = &snapshot.buffer_snapshot;
|
||||
let prev_buffer_hunks = buffer_editor
|
||||
.diff_hunks_in_ranges(&[editor::Anchor::min()..editor::Anchor::max()], snapshot)
|
||||
.collect::<Vec<_>>();
|
||||
buffer_editor.git_restore(&Default::default(), window, cx);
|
||||
prev_buffer_hunks
|
||||
});
|
||||
assert_eq!(prev_buffer_hunks.len(), 1);
|
||||
cx.run_until_parked();
|
||||
|
||||
let new_buffer_hunks =
|
||||
cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| {
|
||||
let snapshot = buffer_editor.snapshot(window, cx);
|
||||
let snapshot = &snapshot.buffer_snapshot;
|
||||
let new_buffer_hunks = buffer_editor
|
||||
.diff_hunks_in_ranges(&[editor::Anchor::min()..editor::Anchor::max()], snapshot)
|
||||
.collect::<Vec<_>>();
|
||||
buffer_editor.git_restore(&Default::default(), window, cx);
|
||||
new_buffer_hunks
|
||||
});
|
||||
assert_eq!(new_buffer_hunks.as_slice(), &[]);
|
||||
|
||||
cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| {
|
||||
buffer_editor.set_text("different\n", window, cx);
|
||||
buffer_editor.save(false, project.clone(), window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_state_with_diff(
|
||||
&diff_editor,
|
||||
cx,
|
||||
&"
|
||||
- original
|
||||
+ ˇdifferent
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
use crate::project_diff::{self, ProjectDiff};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_go_to_prev_hunk_multibuffer(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
".git":{},
|
||||
"a.txt": "created\n",
|
||||
"b.txt": "really changed\n",
|
||||
"c.txt": "unchanged\n"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
fs.set_git_content_for_repo(
|
||||
Path::new("/a/.git"),
|
||||
&[
|
||||
("b.txt".into(), "before\n".to_string(), None),
|
||||
("c.txt".into(), "unchanged\n".to_string(), None),
|
||||
("d.txt".into(), "deleted\n".to_string(), None),
|
||||
],
|
||||
);
|
||||
|
||||
let project = Project::test(fs, [Path::new("/a")], cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.focus(&workspace);
|
||||
cx.update(|window, cx| {
|
||||
window.dispatch_action(project_diff::Diff.boxed_clone(), cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let item = workspace.update(cx, |workspace, cx| {
|
||||
workspace.active_item_as::<ProjectDiff>(cx).unwrap()
|
||||
});
|
||||
cx.focus(&item);
|
||||
let editor = item.update(cx, |item, _| item.editor.clone());
|
||||
|
||||
let mut cx = EditorTestContext::for_editor_in(editor, cx).await;
|
||||
|
||||
cx.assert_excerpts_with_selections(indoc!(
|
||||
"
|
||||
[EXCERPT]
|
||||
before
|
||||
really changed
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
ˇcreated
|
||||
"
|
||||
));
|
||||
|
||||
cx.dispatch_action(editor::actions::GoToPreviousHunk);
|
||||
|
||||
cx.assert_excerpts_with_selections(indoc!(
|
||||
"
|
||||
[EXCERPT]
|
||||
before
|
||||
really changed
|
||||
[EXCERPT]
|
||||
ˇ[FOLDED]
|
||||
[EXCERPT]
|
||||
created
|
||||
"
|
||||
));
|
||||
|
||||
cx.dispatch_action(editor::actions::GoToPreviousHunk);
|
||||
|
||||
cx.assert_excerpts_with_selections(indoc!(
|
||||
"
|
||||
[EXCERPT]
|
||||
ˇbefore
|
||||
really changed
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
created
|
||||
"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ impl RemoteOutputToast {
|
||||
}
|
||||
});
|
||||
|
||||
let message;
|
||||
let mut message: SharedString;
|
||||
let remote;
|
||||
|
||||
match action {
|
||||
@@ -86,19 +86,32 @@ impl RemoteOutputToast {
|
||||
|
||||
RemoteAction::Push(remote_ref) => {
|
||||
message = output.stdout.trim().to_string().into();
|
||||
let remote_message = get_remote_lines(&output.stderr);
|
||||
let finder = LinkFinder::new();
|
||||
let links = finder
|
||||
.links(&remote_message)
|
||||
.filter(|link| *link.kind() == LinkKind::Url)
|
||||
.map(|link| link.start()..link.end())
|
||||
.collect_vec();
|
||||
if message.is_empty() {
|
||||
message = output.stderr.trim().to_string().into();
|
||||
if message.is_empty() {
|
||||
message = "Push Successful".into();
|
||||
}
|
||||
remote = None;
|
||||
} else {
|
||||
let remote_message = get_remote_lines(&output.stderr);
|
||||
|
||||
remote = Some(InfoFromRemote {
|
||||
name: remote_ref.name,
|
||||
remote_text: remote_message.into(),
|
||||
links,
|
||||
});
|
||||
remote = if remote_message.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let finder = LinkFinder::new();
|
||||
let links = finder
|
||||
.links(&remote_message)
|
||||
.filter(|link| *link.kind() == LinkKind::Url)
|
||||
.map(|link| link.start()..link.end())
|
||||
.collect_vec();
|
||||
|
||||
Some(InfoFromRemote {
|
||||
name: remote_ref.name,
|
||||
remote_text: remote_message.into(),
|
||||
links,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,70 +1,49 @@
|
||||
use gpui::{
|
||||
AnyElement, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
Subscription, Task, WeakEntity,
|
||||
AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{
|
||||
git::{GitStore, Repository},
|
||||
Project,
|
||||
};
|
||||
use project::{git::Repository, Project};
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing};
|
||||
|
||||
pub struct RepositorySelector {
|
||||
picker: Entity<Picker<RepositorySelectorDelegate>>,
|
||||
/// The task used to update the picker's matches when there is a change to
|
||||
/// the repository list.
|
||||
update_matches_task: Option<Task<()>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl RepositorySelector {
|
||||
pub fn new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let git_store = project.read(cx).git_store().clone();
|
||||
pub fn new(
|
||||
project_handle: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let project = project_handle.read(cx);
|
||||
let git_store = project.git_store().clone();
|
||||
let all_repositories = git_store.read(cx).all_repositories();
|
||||
let filtered_repositories = all_repositories.clone();
|
||||
|
||||
let widest_item_ix = all_repositories.iter().position_max_by(|a, b| {
|
||||
a.read(cx)
|
||||
.display_name(project, cx)
|
||||
.len()
|
||||
.cmp(&b.read(cx).display_name(project, cx).len())
|
||||
});
|
||||
|
||||
let delegate = RepositorySelectorDelegate {
|
||||
project: project.downgrade(),
|
||||
project: project_handle.downgrade(),
|
||||
repository_selector: cx.entity().downgrade(),
|
||||
repository_entries: all_repositories,
|
||||
repository_entries: all_repositories.clone(),
|
||||
filtered_repositories,
|
||||
selected_index: 0,
|
||||
};
|
||||
|
||||
let picker = cx.new(|cx| {
|
||||
Picker::nonsearchable_uniform_list(delegate, window, cx)
|
||||
.widest_item(widest_item_ix)
|
||||
.max_height(Some(rems(20.).into()))
|
||||
.width(rems(15.))
|
||||
});
|
||||
|
||||
let _subscriptions =
|
||||
vec![cx.subscribe_in(&git_store, window, Self::handle_project_git_event)];
|
||||
|
||||
RepositorySelector {
|
||||
picker,
|
||||
update_matches_task: None,
|
||||
_subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn repositories_len(&self, cx: &App) -> usize {
|
||||
self.picker.read(cx).delegate.repository_entries.len()
|
||||
}
|
||||
|
||||
fn handle_project_git_event(
|
||||
&mut self,
|
||||
git_store: &Entity<GitStore>,
|
||||
_event: &project::git::GitEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// TODO handle events individually
|
||||
let task = self.picker.update(cx, |this, cx| {
|
||||
let query = this.query(cx);
|
||||
this.delegate.repository_entries = git_store.read(cx).all_repositories();
|
||||
this.delegate.update_matches(query, window, cx)
|
||||
});
|
||||
self.update_matches_task = Some(task);
|
||||
RepositorySelector { picker }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,54 +61,6 @@ impl Render for RepositorySelector {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct RepositorySelectorPopoverMenu<T, TT>
|
||||
where
|
||||
T: PopoverTrigger + ButtonCommon,
|
||||
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
{
|
||||
repository_selector: Entity<RepositorySelector>,
|
||||
trigger: T,
|
||||
tooltip: TT,
|
||||
handle: Option<PopoverMenuHandle<RepositorySelector>>,
|
||||
}
|
||||
|
||||
impl<T, TT> RepositorySelectorPopoverMenu<T, TT>
|
||||
where
|
||||
T: PopoverTrigger + ButtonCommon,
|
||||
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
{
|
||||
pub fn new(repository_selector: Entity<RepositorySelector>, trigger: T, tooltip: TT) -> Self {
|
||||
Self {
|
||||
repository_selector,
|
||||
trigger,
|
||||
tooltip,
|
||||
handle: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_handle(mut self, handle: PopoverMenuHandle<RepositorySelector>) -> Self {
|
||||
self.handle = Some(handle);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, TT> RenderOnce for RepositorySelectorPopoverMenu<T, TT>
|
||||
where
|
||||
T: PopoverTrigger + ButtonCommon,
|
||||
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
{
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let repository_selector = self.repository_selector.clone();
|
||||
|
||||
PopoverMenu::new("repository-switcher")
|
||||
.menu(move |_window, _cx| Some(repository_selector.clone()))
|
||||
.trigger_with_tooltip(self.trigger, self.tooltip)
|
||||
.attach(gpui::Corner::BottomLeft)
|
||||
.when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RepositorySelectorDelegate {
|
||||
project: WeakEntity<Project>,
|
||||
repository_selector: WeakEntity<RepositorySelector>,
|
||||
@@ -238,7 +169,6 @@ impl PickerDelegate for RepositorySelectorDelegate {
|
||||
let project = self.project.upgrade()?;
|
||||
let repo_info = self.filtered_repositories.get(ix)?;
|
||||
let display_name = repo_info.read(cx).display_name(project.read(cx), cx);
|
||||
// TODO: Implement repository item rendering
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
|
||||
@@ -90,6 +90,21 @@ impl<'a, T: 'static> Context<'a, T> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Subscribe to an event type from ourself
|
||||
pub fn subscribe_self<Evt>(
|
||||
&mut self,
|
||||
mut on_event: impl FnMut(&mut T, &Evt, &mut Context<'_, T>) + 'static,
|
||||
) -> Subscription
|
||||
where
|
||||
T: 'static + EventEmitter<Evt>,
|
||||
Evt: 'static,
|
||||
{
|
||||
let this = self.entity();
|
||||
self.app.subscribe(&this, move |this, evt, cx| {
|
||||
this.update(cx, |this, cx| on_event(this, evt, cx))
|
||||
})
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when GPUI releases this entity.
|
||||
pub fn on_release(&self, on_release: impl FnOnce(&mut T, &mut App) + 'static) -> Subscription
|
||||
where
|
||||
|
||||
@@ -670,6 +670,14 @@ pub fn pattern_slash(color: Hsla, thickness: f32) -> Background {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a solid background color.
|
||||
pub fn solid_background(color: impl Into<Hsla>) -> Background {
|
||||
Background {
|
||||
solid: color.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a LinearGradient background color.
|
||||
///
|
||||
/// The gradient line's angle of direction. A value of `0.` is equivalent to to top; increasing values rotate clockwise from there.
|
||||
|
||||
@@ -168,6 +168,23 @@ impl Subscription {
|
||||
pub fn detach(mut self) {
|
||||
self.unsubscribe.take();
|
||||
}
|
||||
|
||||
/// Joins two subscriptions into a single subscription. Detach will
|
||||
/// detach both interior subscriptions.
|
||||
pub fn join(mut subscription_a: Self, mut subscription_b: Self) -> Self {
|
||||
let a_unsubscribe = subscription_a.unsubscribe.take();
|
||||
let b_unsubscribe = subscription_b.unsubscribe.take();
|
||||
Self {
|
||||
unsubscribe: Some(Box::new(move || {
|
||||
if let Some(self_unsubscribe) = a_unsubscribe {
|
||||
self_unsubscribe();
|
||||
}
|
||||
if let Some(other_unsubscribe) = b_unsubscribe {
|
||||
other_unsubscribe();
|
||||
}
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Subscription {
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use feature_flags::ZedPro;
|
||||
use gpui::{
|
||||
action_with_deprecated_aliases, Action, AnyElement, App, Corner, DismissEvent, Entity,
|
||||
action_with_deprecated_aliases, Action, AnyElement, AnyView, App, Corner, DismissEvent, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use language_model::{
|
||||
@@ -10,10 +10,7 @@ use language_model::{
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use proto::Plan;
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, IconButtonShape, ListItem, ListItemSpacing, PopoverButton,
|
||||
PopoverMenuHandle, Tooltip, TriggerablePopover,
|
||||
};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
|
||||
use workspace::ShowConfiguration;
|
||||
|
||||
action_with_deprecated_aliases!(
|
||||
@@ -31,7 +28,6 @@ pub struct LanguageModelSelector {
|
||||
/// The task used to update the picker's matches when there is a change to
|
||||
/// the language model registry.
|
||||
update_matches_task: Option<Task<()>>,
|
||||
popover_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
_authenticate_all_providers_task: Task<()>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
@@ -63,7 +59,6 @@ impl LanguageModelSelector {
|
||||
LanguageModelSelector {
|
||||
picker,
|
||||
update_matches_task: None,
|
||||
popover_menu_handle: PopoverMenuHandle::default(),
|
||||
_authenticate_all_providers_task: Self::authenticate_all_providers(cx),
|
||||
_subscriptions: vec![cx.subscribe_in(
|
||||
&LanguageModelRegistry::global(cx),
|
||||
@@ -73,15 +68,6 @@ impl LanguageModelSelector {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_model_selector(
|
||||
&mut self,
|
||||
_: &ToggleModelSelector,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.popover_menu_handle.toggle(window, cx);
|
||||
}
|
||||
|
||||
fn handle_language_model_registry_event(
|
||||
&mut self,
|
||||
_registry: &Entity<LanguageModelRegistry>,
|
||||
@@ -201,13 +187,62 @@ impl Render for LanguageModelSelector {
|
||||
}
|
||||
}
|
||||
|
||||
impl TriggerablePopover for LanguageModelSelector {
|
||||
fn menu_handle(
|
||||
&mut self,
|
||||
_window: &mut Window,
|
||||
_cx: &mut gpui::Context<Self>,
|
||||
) -> PopoverMenuHandle<Self> {
|
||||
self.popover_menu_handle.clone()
|
||||
#[derive(IntoElement)]
|
||||
pub struct LanguageModelSelectorPopoverMenu<T, TT>
|
||||
where
|
||||
T: PopoverTrigger + ButtonCommon,
|
||||
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
{
|
||||
language_model_selector: Entity<LanguageModelSelector>,
|
||||
trigger: T,
|
||||
tooltip: TT,
|
||||
handle: Option<PopoverMenuHandle<LanguageModelSelector>>,
|
||||
anchor: Corner,
|
||||
}
|
||||
|
||||
impl<T, TT> LanguageModelSelectorPopoverMenu<T, TT>
|
||||
where
|
||||
T: PopoverTrigger + ButtonCommon,
|
||||
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
{
|
||||
pub fn new(
|
||||
language_model_selector: Entity<LanguageModelSelector>,
|
||||
trigger: T,
|
||||
tooltip: TT,
|
||||
anchor: Corner,
|
||||
) -> Self {
|
||||
Self {
|
||||
language_model_selector,
|
||||
trigger,
|
||||
tooltip,
|
||||
handle: None,
|
||||
anchor,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_handle(mut self, handle: PopoverMenuHandle<LanguageModelSelector>) -> Self {
|
||||
self.handle = Some(handle);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, TT> RenderOnce for LanguageModelSelectorPopoverMenu<T, TT>
|
||||
where
|
||||
T: PopoverTrigger + ButtonCommon,
|
||||
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
{
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let language_model_selector = self.language_model_selector.clone();
|
||||
|
||||
PopoverMenu::new("model-switcher")
|
||||
.menu(move |_window, _cx| Some(language_model_selector.clone()))
|
||||
.trigger_with_tooltip(self.trigger, self.tooltip)
|
||||
.anchor(self.anchor)
|
||||
.when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle))
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-2.0),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,9 +436,9 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
.pl_0p5()
|
||||
.w(px(240.))
|
||||
.child(
|
||||
div().max_w_40().child(
|
||||
Label::new(model_info.model.name().0.clone()).text_ellipsis(),
|
||||
),
|
||||
div()
|
||||
.max_w_40()
|
||||
.child(Label::new(model_info.model.name().0.clone()).truncate()),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -492,98 +527,3 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InlineLanguageModelSelector {
|
||||
selector: Entity<LanguageModelSelector>,
|
||||
}
|
||||
|
||||
impl InlineLanguageModelSelector {
|
||||
pub fn new(selector: Entity<LanguageModelSelector>) -> Self {
|
||||
Self { selector }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for InlineLanguageModelSelector {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
PopoverButton::new(
|
||||
self.selector,
|
||||
gpui::Corner::TopRight,
|
||||
IconButton::new("context", IconName::SettingsAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted),
|
||||
move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"Using {}",
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.active_model()
|
||||
.map(|model| model.name().0)
|
||||
.unwrap_or_else(|| "No model selected".into()),
|
||||
),
|
||||
None,
|
||||
"Change Model",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
.render(window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AssistantLanguageModelSelector {
|
||||
focus_handle: FocusHandle,
|
||||
selector: Entity<LanguageModelSelector>,
|
||||
}
|
||||
|
||||
impl AssistantLanguageModelSelector {
|
||||
pub fn new(focus_handle: FocusHandle, selector: Entity<LanguageModelSelector>) -> Self {
|
||||
Self {
|
||||
focus_handle,
|
||||
selector,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AssistantLanguageModelSelector {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let model_name = match active_model {
|
||||
Some(model) => model.name().0,
|
||||
_ => SharedString::from("No model selected"),
|
||||
};
|
||||
|
||||
PopoverButton::new(
|
||||
self.selector.clone(),
|
||||
Corner::BottomRight,
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
),
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Change Model",
|
||||
&ToggleModelSelector,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
.render(window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,6 +250,7 @@ impl DiffState {
|
||||
}
|
||||
}
|
||||
BufferDiffEvent::LanguageChanged => this.buffer_diff_language_changed(diff, cx),
|
||||
_ => {}
|
||||
}),
|
||||
diff,
|
||||
}
|
||||
@@ -2045,6 +2046,7 @@ impl MultiBuffer {
|
||||
.cursor::<(Option<&Locator>, ExcerptOffset)>(&());
|
||||
let mut edits = Vec::new();
|
||||
let mut excerpt_ids = ids.iter().copied().peekable();
|
||||
let mut removed_buffer_ids = Vec::new();
|
||||
|
||||
while let Some(excerpt_id) = excerpt_ids.next() {
|
||||
// Seek to the next excerpt to remove, preserving any preceding excerpts.
|
||||
@@ -2062,7 +2064,12 @@ impl MultiBuffer {
|
||||
if let Some(buffer_state) = buffers.get_mut(&excerpt.buffer_id) {
|
||||
buffer_state.excerpts.retain(|l| l != &excerpt.locator);
|
||||
if buffer_state.excerpts.is_empty() {
|
||||
log::debug!(
|
||||
"removing buffer and diff for buffer {}",
|
||||
excerpt.buffer_id
|
||||
);
|
||||
buffers.remove(&excerpt.buffer_id);
|
||||
removed_buffer_ids.push(excerpt.buffer_id);
|
||||
}
|
||||
}
|
||||
cursor.next(&());
|
||||
@@ -2103,6 +2110,10 @@ impl MultiBuffer {
|
||||
new_excerpts.append(suffix, &());
|
||||
drop(cursor);
|
||||
snapshot.excerpts = new_excerpts;
|
||||
for buffer_id in removed_buffer_ids {
|
||||
self.diffs.remove(&buffer_id);
|
||||
snapshot.diffs.remove(&buffer_id);
|
||||
}
|
||||
|
||||
if changed_trailing_excerpt {
|
||||
snapshot.trailing_excerpt_update_count += 1;
|
||||
@@ -2716,6 +2727,12 @@ impl MultiBuffer {
|
||||
snapshot.has_deleted_file = has_deleted_file;
|
||||
snapshot.has_conflict = has_conflict;
|
||||
|
||||
for (id, diff) in self.diffs.iter() {
|
||||
if snapshot.diffs.get(&id).is_none() {
|
||||
snapshot.diffs.insert(*id, diff.diff.read(cx).snapshot(cx));
|
||||
}
|
||||
}
|
||||
|
||||
excerpts_to_edit.sort_unstable_by_key(|(locator, _, _)| *locator);
|
||||
|
||||
let mut edits = Vec::new();
|
||||
@@ -3476,7 +3493,10 @@ impl MultiBufferSnapshot {
|
||||
) -> impl Iterator<Item = MultiBufferDiffHunk> + '_ {
|
||||
let query_range = range.start.to_point(self)..range.end.to_point(self);
|
||||
self.lift_buffer_metadata(query_range.clone(), move |buffer, buffer_range| {
|
||||
let diff = self.diffs.get(&buffer.remote_id())?;
|
||||
let Some(diff) = self.diffs.get(&buffer.remote_id()) else {
|
||||
log::debug!("no diff found for {:?}", buffer.remote_id());
|
||||
return None;
|
||||
};
|
||||
let buffer_start = buffer.anchor_before(buffer_range.start);
|
||||
let buffer_end = buffer.anchor_after(buffer_range.end);
|
||||
Some(
|
||||
@@ -3485,17 +3505,12 @@ impl MultiBufferSnapshot {
|
||||
if hunk.is_created_file() && !self.all_diff_hunks_expanded {
|
||||
return None;
|
||||
}
|
||||
Some((
|
||||
Point::new(hunk.row_range.start, 0)..Point::new(hunk.row_range.end, 0),
|
||||
hunk,
|
||||
))
|
||||
Some((hunk.range.clone(), hunk))
|
||||
}),
|
||||
)
|
||||
})
|
||||
.filter_map(move |(range, hunk, excerpt)| {
|
||||
if range.start != range.end
|
||||
&& range.end == query_range.start
|
||||
&& !hunk.row_range.is_empty()
|
||||
if range.start != range.end && range.end == query_range.start && !hunk.range.is_empty()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -3790,104 +3805,57 @@ impl MultiBufferSnapshot {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn diff_hunk_before<T: ToOffset>(&self, position: T) -> Option<MultiBufferDiffHunk> {
|
||||
pub fn diff_hunk_before<T: ToOffset>(&self, position: T) -> Option<MultiBufferRow> {
|
||||
let offset = position.to_offset(self);
|
||||
|
||||
// Go to the region containing the given offset.
|
||||
let mut cursor = self.cursor::<DimensionPair<usize, Point>>();
|
||||
cursor.seek(&DimensionPair {
|
||||
key: offset,
|
||||
value: None,
|
||||
});
|
||||
let mut region = cursor.region()?;
|
||||
if region.range.start.key == offset || !region.is_main_buffer {
|
||||
cursor.prev();
|
||||
region = cursor.region()?;
|
||||
cursor.seek_to_start_of_current_excerpt();
|
||||
let excerpt = cursor.excerpt()?;
|
||||
|
||||
let excerpt_end = excerpt.range.context.end.to_offset(&excerpt.buffer);
|
||||
let current_position = self
|
||||
.anchor_before(offset)
|
||||
.text_anchor
|
||||
.to_offset(&excerpt.buffer);
|
||||
let excerpt_end = excerpt
|
||||
.buffer
|
||||
.anchor_before(excerpt_end.min(current_position));
|
||||
|
||||
if let Some(diff) = self.diffs.get(&excerpt.buffer_id) {
|
||||
for hunk in diff.hunks_intersecting_range_rev(
|
||||
excerpt.range.context.start..excerpt_end,
|
||||
&excerpt.buffer,
|
||||
) {
|
||||
let hunk_end = hunk.buffer_range.end.to_offset(&excerpt.buffer);
|
||||
if hunk_end >= current_position {
|
||||
continue;
|
||||
}
|
||||
let start =
|
||||
Anchor::in_buffer(excerpt.id, excerpt.buffer_id, hunk.buffer_range.start)
|
||||
.to_point(&self);
|
||||
return Some(MultiBufferRow(start.row));
|
||||
}
|
||||
}
|
||||
|
||||
// Find the corresponding buffer offset.
|
||||
let overshoot = if region.is_main_buffer {
|
||||
offset - region.range.start.key
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let mut max_buffer_offset = region
|
||||
.buffer
|
||||
.clip_offset(region.buffer_range.start.key + overshoot, Bias::Right);
|
||||
|
||||
loop {
|
||||
let excerpt = cursor.excerpt()?;
|
||||
let excerpt_end = excerpt.range.context.end.to_offset(&excerpt.buffer);
|
||||
let buffer_offset = excerpt_end.min(max_buffer_offset);
|
||||
let buffer_end = excerpt.buffer.anchor_before(buffer_offset);
|
||||
let buffer_end_row = buffer_end.to_point(&excerpt.buffer).row;
|
||||
|
||||
if let Some(diff) = self.diffs.get(&excerpt.buffer_id) {
|
||||
for hunk in diff.hunks_intersecting_range_rev(
|
||||
excerpt.range.context.start..buffer_end,
|
||||
&excerpt.buffer,
|
||||
) {
|
||||
let hunk_range = hunk.buffer_range.to_offset(&excerpt.buffer);
|
||||
if hunk.row_range.end >= buffer_end_row {
|
||||
continue;
|
||||
}
|
||||
|
||||
let hunk_start = Point::new(hunk.row_range.start, 0);
|
||||
let hunk_end = Point::new(hunk.row_range.end, 0);
|
||||
|
||||
cursor.seek_to_buffer_position_in_current_excerpt(&DimensionPair {
|
||||
key: hunk_range.start,
|
||||
value: None,
|
||||
});
|
||||
|
||||
let mut region = cursor.region()?;
|
||||
while !region.is_main_buffer || region.buffer_range.start.key >= hunk_range.end
|
||||
{
|
||||
cursor.prev();
|
||||
region = cursor.region()?;
|
||||
}
|
||||
|
||||
let overshoot = if region.is_main_buffer {
|
||||
hunk_start.saturating_sub(region.buffer_range.start.value.unwrap())
|
||||
} else {
|
||||
Point::zero()
|
||||
};
|
||||
let start = region.range.start.value.unwrap() + overshoot;
|
||||
|
||||
while let Some(region) = cursor.region() {
|
||||
if !region.is_main_buffer
|
||||
|| region.buffer_range.end.value.unwrap() <= hunk_end
|
||||
{
|
||||
cursor.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let end = if let Some(region) = cursor.region() {
|
||||
let overshoot = if region.is_main_buffer {
|
||||
hunk_end.saturating_sub(region.buffer_range.start.value.unwrap())
|
||||
} else {
|
||||
Point::zero()
|
||||
};
|
||||
region.range.start.value.unwrap() + overshoot
|
||||
} else {
|
||||
self.max_point()
|
||||
};
|
||||
|
||||
return Some(MultiBufferDiffHunk {
|
||||
row_range: MultiBufferRow(start.row)..MultiBufferRow(end.row),
|
||||
buffer_id: excerpt.buffer_id,
|
||||
excerpt_id: excerpt.id,
|
||||
buffer_range: hunk.buffer_range.clone(),
|
||||
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
|
||||
secondary_status: hunk.secondary_status,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cursor.prev_excerpt();
|
||||
max_buffer_offset = usize::MAX;
|
||||
let excerpt = cursor.excerpt()?;
|
||||
|
||||
let Some(diff) = self.diffs.get(&excerpt.buffer_id) else {
|
||||
continue;
|
||||
};
|
||||
let mut hunks =
|
||||
diff.hunks_intersecting_range_rev(excerpt.range.context.clone(), &excerpt.buffer);
|
||||
let Some(hunk) = hunks.next() else {
|
||||
continue;
|
||||
};
|
||||
let start = Anchor::in_buffer(excerpt.id, excerpt.buffer_id, hunk.buffer_range.start)
|
||||
.to_point(&self);
|
||||
return Some(MultiBufferRow(start.row));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6090,21 +6058,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn seek_to_buffer_position_in_current_excerpt(&mut self, position: &D) {
|
||||
self.cached_region.take();
|
||||
if let Some(excerpt) = self.excerpts.item() {
|
||||
let excerpt_start = excerpt.range.context.start.summary::<D>(&excerpt.buffer);
|
||||
let position_in_excerpt = *position - excerpt_start;
|
||||
let mut excerpt_position = self.excerpts.start().0;
|
||||
excerpt_position.add_assign(&position_in_excerpt);
|
||||
self.diff_transforms
|
||||
.seek(&ExcerptDimension(excerpt_position), Bias::Left, &());
|
||||
if self.diff_transforms.item().is_none() {
|
||||
self.diff_transforms.next(&());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn next_excerpt(&mut self) {
|
||||
self.excerpts.next(&());
|
||||
self.seek_to_start_of_current_excerpt();
|
||||
|
||||
@@ -440,23 +440,14 @@ fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
|
||||
vec![1..3, 4..6, 7..8]
|
||||
);
|
||||
|
||||
assert_eq!(snapshot.diff_hunk_before(Point::new(1, 1)), None,);
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.diff_hunk_before(Point::new(1, 1))
|
||||
.map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0),
|
||||
None,
|
||||
snapshot.diff_hunk_before(Point::new(7, 0)),
|
||||
Some(MultiBufferRow(4))
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.diff_hunk_before(Point::new(7, 0))
|
||||
.map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0),
|
||||
Some(4..6)
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.diff_hunk_before(Point::new(4, 0))
|
||||
.map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0),
|
||||
Some(1..3)
|
||||
snapshot.diff_hunk_before(Point::new(4, 0)),
|
||||
Some(MultiBufferRow(1))
|
||||
);
|
||||
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
@@ -478,16 +469,12 @@ fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.diff_hunk_before(Point::new(2, 0))
|
||||
.map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0),
|
||||
Some(1..1),
|
||||
snapshot.diff_hunk_before(Point::new(2, 0)),
|
||||
Some(MultiBufferRow(1)),
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.diff_hunk_before(Point::new(4, 0))
|
||||
.map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0),
|
||||
Some(2..2)
|
||||
snapshot.diff_hunk_before(Point::new(4, 0)),
|
||||
Some(MultiBufferRow(2))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2160,6 +2147,7 @@ impl ReferenceMultibuffer {
|
||||
.unwrap();
|
||||
let excerpt = self.excerpts.remove(ix);
|
||||
let buffer = excerpt.buffer.read(cx);
|
||||
let id = buffer.remote_id();
|
||||
log::info!(
|
||||
"Removing excerpt {}: {:?}",
|
||||
ix,
|
||||
@@ -2167,6 +2155,13 @@ impl ReferenceMultibuffer {
|
||||
.text_for_range(excerpt.range.to_offset(buffer))
|
||||
.collect::<String>(),
|
||||
);
|
||||
if !self
|
||||
.excerpts
|
||||
.iter()
|
||||
.any(|excerpt| excerpt.buffer.read(cx).remote_id() == id)
|
||||
{
|
||||
self.diffs.remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_excerpt_after(
|
||||
@@ -2266,7 +2261,7 @@ impl ReferenceMultibuffer {
|
||||
}
|
||||
|
||||
if !hunk.buffer_range.start.is_valid(&buffer) {
|
||||
log::trace!("skipping hunk with deleted start: {:?}", hunk.row_range);
|
||||
log::trace!("skipping hunk with deleted start: {:?}", hunk.range);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2415,6 +2410,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
.unwrap_or(10);
|
||||
|
||||
let mut buffers: Vec<Entity<Buffer>> = Vec::new();
|
||||
let mut base_texts: HashMap<BufferId, String> = HashMap::default();
|
||||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||||
let mut reference = ReferenceMultibuffer::default();
|
||||
let mut anchors = Vec::new();
|
||||
@@ -2522,9 +2518,10 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
..snapshot.anchor_in_excerpt(excerpt.id, end).unwrap();
|
||||
|
||||
log::info!(
|
||||
"expanding diff hunks in range {:?} (excerpt id {:?}) index {excerpt_ix:?})",
|
||||
"expanding diff hunks in range {:?} (excerpt id {:?}, index {excerpt_ix:?}, buffer id {:?})",
|
||||
range.to_offset(&snapshot),
|
||||
excerpt.id
|
||||
excerpt.id,
|
||||
excerpt.buffer.read(cx).remote_id(),
|
||||
);
|
||||
reference.expand_diff_hunks(excerpt.id, start..end, cx);
|
||||
multibuffer.expand_diff_hunks(vec![range], cx);
|
||||
@@ -2534,7 +2531,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
for buffer in multibuffer.all_buffers() {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let _ = multibuffer.diff_for(snapshot.remote_id()).unwrap().update(
|
||||
multibuffer.diff_for(snapshot.remote_id()).unwrap().update(
|
||||
cx,
|
||||
|diff, cx| {
|
||||
log::info!(
|
||||
@@ -2551,17 +2548,16 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
}
|
||||
_ => {
|
||||
let buffer_handle = if buffers.is_empty() || rng.gen_bool(0.4) {
|
||||
let base_text = util::RandomCharIter::new(&mut rng)
|
||||
let mut base_text = util::RandomCharIter::new(&mut rng)
|
||||
.take(256)
|
||||
.collect::<String>();
|
||||
|
||||
let buffer = cx.new(|cx| Buffer::local(base_text.clone(), cx));
|
||||
let diff = cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer, cx));
|
||||
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
reference.add_diff(diff.clone(), cx);
|
||||
multibuffer.add_diff(diff, cx)
|
||||
});
|
||||
text::LineEnding::normalize(&mut base_text);
|
||||
base_texts.insert(
|
||||
buffer.read_with(cx, |buffer, _| buffer.remote_id()),
|
||||
base_text,
|
||||
);
|
||||
buffers.push(buffer);
|
||||
buffers.last().unwrap()
|
||||
} else {
|
||||
@@ -2595,6 +2591,18 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
(start_ix..end_ix, anchor_range)
|
||||
});
|
||||
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
let id = buffer_handle.read(cx).remote_id();
|
||||
if multibuffer.diff_for(id).is_none() {
|
||||
let base_text = base_texts.get(&id).unwrap();
|
||||
let diff = cx.new(|cx| {
|
||||
BufferDiff::new_with_base_text(base_text, &buffer_handle, cx)
|
||||
});
|
||||
reference.add_diff(diff.clone(), cx);
|
||||
multibuffer.add_diff(diff, cx)
|
||||
}
|
||||
});
|
||||
|
||||
let excerpt_id = multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer
|
||||
.insert_excerpts_after(
|
||||
|
||||
@@ -84,9 +84,10 @@ pub fn panel_editor_style(monospace: bool, window: &Window, cx: &App) -> EditorS
|
||||
|
||||
let font_size = TextSize::Small.rems(cx).to_pixels(window.rem_size());
|
||||
|
||||
let (font_family, font_features, font_weight, line_height) = if monospace {
|
||||
let (font_family, font_fallbacks, font_features, font_weight, line_height) = if monospace {
|
||||
(
|
||||
settings.buffer_font.family.clone(),
|
||||
settings.buffer_font.fallbacks.clone(),
|
||||
settings.buffer_font.features.clone(),
|
||||
settings.buffer_font.weight,
|
||||
font_size * settings.buffer_line_height.value(),
|
||||
@@ -94,6 +95,7 @@ pub fn panel_editor_style(monospace: bool, window: &Window, cx: &App) -> EditorS
|
||||
} else {
|
||||
(
|
||||
settings.ui_font.family.clone(),
|
||||
settings.ui_font.fallbacks.clone(),
|
||||
settings.ui_font.features.clone(),
|
||||
settings.ui_font.weight,
|
||||
window.line_height(),
|
||||
@@ -106,6 +108,7 @@ pub fn panel_editor_style(monospace: bool, window: &Window, cx: &App) -> EditorS
|
||||
text: TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family,
|
||||
font_fallbacks,
|
||||
font_features,
|
||||
font_size: TextSize::Small.rems(cx).into(),
|
||||
font_weight,
|
||||
|
||||
@@ -48,6 +48,7 @@ pub struct Picker<D: PickerDelegate> {
|
||||
pending_update_matches: Option<PendingUpdateMatches>,
|
||||
confirm_on_update: Option<bool>,
|
||||
width: Option<Length>,
|
||||
widest_item: Option<usize>,
|
||||
max_height: Option<Length>,
|
||||
focus_handle: FocusHandle,
|
||||
/// An external control to display a scrollbar in the `Picker`.
|
||||
@@ -283,6 +284,7 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
pending_update_matches: None,
|
||||
confirm_on_update: None,
|
||||
width: None,
|
||||
widest_item: None,
|
||||
max_height: Some(rems(18.).into()),
|
||||
focus_handle,
|
||||
show_scrollbar: false,
|
||||
@@ -332,6 +334,11 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn widest_item(mut self, ix: Option<usize>) -> Self {
|
||||
self.widest_item = ix;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max_height(mut self, max_height: Option<gpui::Length>) -> Self {
|
||||
self.max_height = max_height;
|
||||
self
|
||||
@@ -690,6 +697,9 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
},
|
||||
)
|
||||
.with_sizing_behavior(sizing_behavior)
|
||||
.when_some(self.widest_item, |el, widest_item| {
|
||||
el.with_width_from_item(Some(widest_item))
|
||||
})
|
||||
.flex_grow()
|
||||
.py_1()
|
||||
.track_scroll(scroll_handle.clone())
|
||||
|
||||
@@ -27,11 +27,13 @@ test-support = [
|
||||
[dependencies]
|
||||
aho-corasick.workspace = true
|
||||
anyhow.workspace = true
|
||||
askpass.workspace = true
|
||||
async-trait.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
fancy-regex.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
@@ -39,25 +41,22 @@ git.workspace = true
|
||||
globset.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
image.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
node_runtime.workspace = true
|
||||
image.workspace = true
|
||||
parking_lot.workspace = true
|
||||
pathdiff.workspace = true
|
||||
paths.workspace = true
|
||||
postage.workspace = true
|
||||
prettier.workspace = true
|
||||
worktree.workspace = true
|
||||
rand.workspace = true
|
||||
regex.workspace = true
|
||||
remote.workspace = true
|
||||
rpc.workspace = true
|
||||
schemars.workspace = true
|
||||
task.workspace = true
|
||||
tempfile.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
@@ -67,13 +66,15 @@ shlex.workspace = true
|
||||
smol.workspace = true
|
||||
snippet.workspace = true
|
||||
snippet_provider.workspace = true
|
||||
task.workspace = true
|
||||
tempfile.workspace = true
|
||||
terminal.workspace = true
|
||||
text.workspace = true
|
||||
toml.workspace = true
|
||||
util.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
which.workspace = true
|
||||
fancy-regex.workspace = true
|
||||
worktree.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -136,6 +136,15 @@ impl BufferDiffState {
|
||||
let _ = self.diff_bases_changed(buffer, diff_bases_change, cx);
|
||||
}
|
||||
|
||||
pub fn wait_for_recalculation(&mut self) -> Option<oneshot::Receiver<()>> {
|
||||
if self.diff_updated_futures.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.diff_updated_futures.push(tx);
|
||||
Some(rx)
|
||||
}
|
||||
|
||||
fn diff_bases_changed(
|
||||
&mut self,
|
||||
buffer: text::BufferSnapshot,
|
||||
@@ -330,6 +339,7 @@ enum OpenBuffer {
|
||||
|
||||
pub enum BufferStoreEvent {
|
||||
BufferAdded(Entity<Buffer>),
|
||||
BufferDiffAdded(Entity<BufferDiff>),
|
||||
BufferDropped(BufferId),
|
||||
BufferChangedFilePath {
|
||||
buffer: Entity<Buffer>,
|
||||
@@ -1362,8 +1372,23 @@ impl BufferStore {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Entity<BufferDiff>>> {
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
if let Some(diff) = self.get_unstaged_diff(buffer_id, cx) {
|
||||
return Task::ready(Ok(diff));
|
||||
if let Some(OpenBuffer::Complete { diff_state, .. }) = self.opened_buffers.get(&buffer_id) {
|
||||
if let Some(unstaged_diff) = diff_state
|
||||
.read(cx)
|
||||
.unstaged_diff
|
||||
.as_ref()
|
||||
.and_then(|weak| weak.upgrade())
|
||||
{
|
||||
if let Some(task) =
|
||||
diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
|
||||
{
|
||||
return cx.background_executor().spawn(async move {
|
||||
task.await?;
|
||||
Ok(unstaged_diff)
|
||||
});
|
||||
}
|
||||
return Task::ready(Ok(unstaged_diff));
|
||||
}
|
||||
}
|
||||
|
||||
let task = match self.loading_diffs.entry((buffer_id, DiffKind::Unstaged)) {
|
||||
@@ -1402,8 +1427,24 @@ impl BufferStore {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Entity<BufferDiff>>> {
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
if let Some(diff) = self.get_uncommitted_diff(buffer_id, cx) {
|
||||
return Task::ready(Ok(diff));
|
||||
|
||||
if let Some(OpenBuffer::Complete { diff_state, .. }) = self.opened_buffers.get(&buffer_id) {
|
||||
if let Some(uncommitted_diff) = diff_state
|
||||
.read(cx)
|
||||
.uncommitted_diff
|
||||
.as_ref()
|
||||
.and_then(|weak| weak.upgrade())
|
||||
{
|
||||
if let Some(task) =
|
||||
diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
|
||||
{
|
||||
return cx.background_executor().spawn(async move {
|
||||
task.await?;
|
||||
Ok(uncommitted_diff)
|
||||
});
|
||||
}
|
||||
return Task::ready(Ok(uncommitted_diff));
|
||||
}
|
||||
}
|
||||
|
||||
let task = match self.loading_diffs.entry((buffer_id, DiffKind::Uncommitted)) {
|
||||
@@ -1482,11 +1523,12 @@ impl BufferStore {
|
||||
if let Some(OpenBuffer::Complete { diff_state, .. }) =
|
||||
this.opened_buffers.get_mut(&buffer_id)
|
||||
{
|
||||
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
|
||||
cx.emit(BufferStoreEvent::BufferDiffAdded(diff.clone()));
|
||||
diff_state.update(cx, |diff_state, cx| {
|
||||
diff_state.language = language;
|
||||
diff_state.language_registry = language_registry;
|
||||
|
||||
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
|
||||
match kind {
|
||||
DiffKind::Unstaged => diff_state.unstaged_diff = Some(diff.downgrade()),
|
||||
DiffKind::Uncommitted => {
|
||||
|
||||
@@ -1,39 +1,53 @@
|
||||
use crate::buffer_store::BufferStore;
|
||||
use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
|
||||
use crate::{Project, ProjectPath};
|
||||
use crate::{
|
||||
buffer_store::{BufferStore, BufferStoreEvent},
|
||||
worktree_store::{WorktreeStore, WorktreeStoreEvent},
|
||||
Project, ProjectItem, ProjectPath,
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use askpass::{AskPassDelegate, AskPassSession};
|
||||
use buffer_diff::BufferDiffEvent;
|
||||
use client::ProjectId;
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use futures::StreamExt as _;
|
||||
use git::repository::{Branch, CommitDetails, PushOptions, Remote, RemoteCommandOutput, ResetMode};
|
||||
use collections::HashMap;
|
||||
use futures::{
|
||||
channel::{mpsc, oneshot},
|
||||
StreamExt as _,
|
||||
};
|
||||
use git::{
|
||||
repository::{GitRepository, RepoPath},
|
||||
status::{GitSummary, TrackedSummary},
|
||||
repository::{
|
||||
Branch, CommitDetails, GitRepository, PushOptions, Remote, RemoteCommandOutput, RepoPath,
|
||||
ResetMode,
|
||||
},
|
||||
status::FileStatus,
|
||||
};
|
||||
use gpui::{
|
||||
App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
|
||||
WeakEntity,
|
||||
};
|
||||
use language::{Buffer, LanguageRegistry};
|
||||
use rpc::proto::{git_reset, ToProto};
|
||||
use rpc::{proto, AnyProtoClient, TypedEnvelope};
|
||||
use parking_lot::Mutex;
|
||||
use rpc::{
|
||||
proto::{self, git_reset, ToProto},
|
||||
AnyProtoClient, TypedEnvelope,
|
||||
};
|
||||
use settings::WorktreeId;
|
||||
use std::collections::VecDeque;
|
||||
use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
future::Future,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use text::BufferId;
|
||||
use util::{maybe, ResultExt};
|
||||
use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry};
|
||||
use util::{debug_panic, maybe, ResultExt};
|
||||
use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry, WorkDirectory};
|
||||
|
||||
pub struct GitStore {
|
||||
buffer_store: Entity<BufferStore>,
|
||||
pub(super) project_id: Option<ProjectId>,
|
||||
pub(super) client: Option<AnyProtoClient>,
|
||||
pub(super) client: AnyProtoClient,
|
||||
repositories: Vec<Entity<Repository>>,
|
||||
active_index: Option<usize>,
|
||||
update_sender: mpsc::UnboundedSender<GitJob>,
|
||||
_subscription: Subscription,
|
||||
_subscriptions: [Subscription; 2],
|
||||
}
|
||||
|
||||
pub struct Repository {
|
||||
@@ -44,6 +58,8 @@ pub struct Repository {
|
||||
pub git_repo: GitRepo,
|
||||
pub merge_message: Option<String>,
|
||||
job_sender: mpsc::UnboundedSender<GitJob>,
|
||||
askpass_delegates: Arc<Mutex<HashMap<u64, AskPassDelegate>>>,
|
||||
latest_askpass_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -57,10 +73,12 @@ pub enum GitRepo {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GitEvent {
|
||||
ActiveRepositoryChanged,
|
||||
FileSystemUpdated,
|
||||
GitStateUpdated,
|
||||
IndexWriteError(anyhow::Error),
|
||||
}
|
||||
|
||||
struct GitJob {
|
||||
@@ -79,12 +97,15 @@ impl GitStore {
|
||||
pub fn new(
|
||||
worktree_store: &Entity<WorktreeStore>,
|
||||
buffer_store: Entity<BufferStore>,
|
||||
client: Option<AnyProtoClient>,
|
||||
client: AnyProtoClient,
|
||||
project_id: Option<ProjectId>,
|
||||
cx: &mut Context<'_, Self>,
|
||||
) -> Self {
|
||||
let update_sender = Self::spawn_git_worker(cx);
|
||||
let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event);
|
||||
let _subscriptions = [
|
||||
cx.subscribe(worktree_store, Self::on_worktree_store_event),
|
||||
cx.subscribe(&buffer_store, Self::on_buffer_store_event),
|
||||
];
|
||||
|
||||
GitStore {
|
||||
project_id,
|
||||
@@ -93,12 +114,15 @@ impl GitStore {
|
||||
repositories: Vec::new(),
|
||||
active_index: None,
|
||||
update_sender,
|
||||
_subscription,
|
||||
_subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(client: &AnyProtoClient) {
|
||||
client.add_entity_request_handler(Self::handle_get_remotes);
|
||||
client.add_entity_request_handler(Self::handle_get_branches);
|
||||
client.add_entity_request_handler(Self::handle_change_branch);
|
||||
client.add_entity_request_handler(Self::handle_create_branch);
|
||||
client.add_entity_request_handler(Self::handle_push);
|
||||
client.add_entity_request_handler(Self::handle_pull);
|
||||
client.add_entity_request_handler(Self::handle_fetch);
|
||||
@@ -110,6 +134,8 @@ impl GitStore {
|
||||
client.add_entity_request_handler(Self::handle_checkout_files);
|
||||
client.add_entity_request_handler(Self::handle_open_commit_message_buffer);
|
||||
client.add_entity_request_handler(Self::handle_set_index_text);
|
||||
client.add_entity_request_handler(Self::handle_askpass);
|
||||
client.add_entity_request_handler(Self::handle_check_for_pushed_commits);
|
||||
}
|
||||
|
||||
pub fn active_repository(&self) -> Option<Entity<Repository>> {
|
||||
@@ -144,7 +170,7 @@ impl GitStore {
|
||||
)
|
||||
})
|
||||
.or_else(|| {
|
||||
let client = client.clone()?;
|
||||
let client = client.clone();
|
||||
let project_id = project_id?;
|
||||
Some((
|
||||
GitRepo::Remote {
|
||||
@@ -196,6 +222,8 @@ impl GitStore {
|
||||
cx.new(|_| Repository {
|
||||
git_store: this.clone(),
|
||||
worktree_id,
|
||||
askpass_delegates: Default::default(),
|
||||
latest_askpass_id: 0,
|
||||
repository_entry: repo.clone(),
|
||||
git_repo,
|
||||
job_sender: self.update_sender.clone(),
|
||||
@@ -226,10 +254,82 @@ impl GitStore {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_buffer_store_event(
|
||||
&mut self,
|
||||
_: Entity<BufferStore>,
|
||||
event: &BufferStoreEvent,
|
||||
cx: &mut Context<'_, Self>,
|
||||
) {
|
||||
if let BufferStoreEvent::BufferDiffAdded(diff) = event {
|
||||
cx.subscribe(diff, Self::on_buffer_diff_event).detach();
|
||||
}
|
||||
}
|
||||
|
||||
fn on_buffer_diff_event(
|
||||
this: &mut GitStore,
|
||||
diff: Entity<buffer_diff::BufferDiff>,
|
||||
event: &BufferDiffEvent,
|
||||
cx: &mut Context<'_, GitStore>,
|
||||
) {
|
||||
if let BufferDiffEvent::HunksStagedOrUnstaged(new_index_text) = event {
|
||||
let buffer_id = diff.read(cx).buffer_id;
|
||||
if let Some((repo, path)) = this.repository_and_path_for_buffer_id(buffer_id, cx) {
|
||||
let recv = repo
|
||||
.read(cx)
|
||||
.set_index_text(&path, new_index_text.as_ref().map(|rope| rope.to_string()));
|
||||
let diff = diff.downgrade();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if let Some(result) = cx.background_spawn(async move { recv.await.ok() }).await
|
||||
{
|
||||
if let Err(error) = result {
|
||||
diff.update(&mut cx, |diff, cx| {
|
||||
diff.clear_pending_hunks(cx);
|
||||
})
|
||||
.ok();
|
||||
this.update(&mut cx, |_, cx| cx.emit(GitEvent::IndexWriteError(error)))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_repositories(&self) -> Vec<Entity<Repository>> {
|
||||
self.repositories.clone()
|
||||
}
|
||||
|
||||
pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option<FileStatus> {
|
||||
let (repo, path) = self.repository_and_path_for_buffer_id(buffer_id, cx)?;
|
||||
let status = repo.read(cx).repository_entry.status_for_path(&path)?;
|
||||
Some(status.status)
|
||||
}
|
||||
|
||||
fn repository_and_path_for_buffer_id(
|
||||
&self,
|
||||
buffer_id: BufferId,
|
||||
cx: &App,
|
||||
) -> Option<(Entity<Repository>, RepoPath)> {
|
||||
let buffer = self.buffer_store.read(cx).get(buffer_id)?;
|
||||
let path = buffer.read(cx).project_path(cx)?;
|
||||
let mut result: Option<(Entity<Repository>, RepoPath)> = None;
|
||||
for repo_handle in &self.repositories {
|
||||
let repo = repo_handle.read(cx);
|
||||
if repo.worktree_id == path.worktree_id {
|
||||
if let Ok(relative_path) = repo.repository_entry.relativize(&path.path) {
|
||||
if result
|
||||
.as_ref()
|
||||
.is_none_or(|(result, _)| !repo.contains_sub_repo(result, cx))
|
||||
{
|
||||
result = Some((repo_handle.clone(), relative_path))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn spawn_git_worker(cx: &mut Context<'_, GitStore>) -> mpsc::UnboundedSender<GitJob> {
|
||||
let (job_tx, mut job_rx) = mpsc::unbounded::<GitJob>();
|
||||
|
||||
@@ -270,9 +370,21 @@ impl GitStore {
|
||||
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
|
||||
let repository_handle =
|
||||
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
|
||||
let askpass_id = envelope.payload.askpass_id;
|
||||
|
||||
let askpass = make_remote_delegate(
|
||||
this,
|
||||
envelope.payload.project_id,
|
||||
worktree_id,
|
||||
work_directory_id,
|
||||
askpass_id,
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
let remote_output = repository_handle
|
||||
.update(&mut cx, |repository_handle, _cx| repository_handle.fetch())?
|
||||
.update(&mut cx, |repository_handle, cx| {
|
||||
repository_handle.fetch(askpass, cx)
|
||||
})?
|
||||
.await??;
|
||||
|
||||
Ok(proto::RemoteMessageResponse {
|
||||
@@ -291,6 +403,16 @@ impl GitStore {
|
||||
let repository_handle =
|
||||
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
|
||||
|
||||
let askpass_id = envelope.payload.askpass_id;
|
||||
let askpass = make_remote_delegate(
|
||||
this,
|
||||
envelope.payload.project_id,
|
||||
worktree_id,
|
||||
work_directory_id,
|
||||
askpass_id,
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
let options = envelope
|
||||
.payload
|
||||
.options
|
||||
@@ -304,8 +426,8 @@ impl GitStore {
|
||||
let remote_name = envelope.payload.remote_name.into();
|
||||
|
||||
let remote_output = repository_handle
|
||||
.update(&mut cx, |repository_handle, _cx| {
|
||||
repository_handle.push(branch_name, remote_name, options)
|
||||
.update(&mut cx, |repository_handle, cx| {
|
||||
repository_handle.push(branch_name, remote_name, options, askpass, cx)
|
||||
})?
|
||||
.await??;
|
||||
Ok(proto::RemoteMessageResponse {
|
||||
@@ -323,15 +445,25 @@ impl GitStore {
|
||||
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
|
||||
let repository_handle =
|
||||
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
|
||||
let askpass_id = envelope.payload.askpass_id;
|
||||
let askpass = make_remote_delegate(
|
||||
this,
|
||||
envelope.payload.project_id,
|
||||
worktree_id,
|
||||
work_directory_id,
|
||||
askpass_id,
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
let branch_name = envelope.payload.branch_name.into();
|
||||
let remote_name = envelope.payload.remote_name.into();
|
||||
|
||||
let remote_message = repository_handle
|
||||
.update(&mut cx, |repository_handle, _cx| {
|
||||
repository_handle.pull(branch_name, remote_name)
|
||||
.update(&mut cx, |repository_handle, cx| {
|
||||
repository_handle.pull(branch_name, remote_name, askpass, cx)
|
||||
})?
|
||||
.await??;
|
||||
|
||||
Ok(proto::RemoteMessageResponse {
|
||||
stdout: remote_message.stdout,
|
||||
stderr: remote_message.stderr,
|
||||
@@ -462,6 +594,67 @@ impl GitStore {
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_get_branches(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::GitGetBranches>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::GitBranchesResponse> {
|
||||
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
|
||||
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
|
||||
let repository_handle =
|
||||
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
|
||||
|
||||
let branches = repository_handle
|
||||
.update(&mut cx, |repository_handle, _| repository_handle.branches())?
|
||||
.await??;
|
||||
|
||||
Ok(proto::GitBranchesResponse {
|
||||
branches: branches
|
||||
.into_iter()
|
||||
.map(|branch| worktree::branch_to_proto(&branch))
|
||||
.collect::<Vec<_>>(),
|
||||
})
|
||||
}
|
||||
async fn handle_create_branch(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::GitCreateBranch>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::Ack> {
|
||||
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
|
||||
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
|
||||
let repository_handle =
|
||||
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
|
||||
let branch_name = envelope.payload.branch_name;
|
||||
|
||||
repository_handle
|
||||
.update(&mut cx, |repository_handle, _| {
|
||||
repository_handle.create_branch(branch_name)
|
||||
})?
|
||||
.await??;
|
||||
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn handle_change_branch(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::GitChangeBranch>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::Ack> {
|
||||
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
|
||||
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
|
||||
let repository_handle =
|
||||
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
|
||||
let branch_name = envelope.payload.branch_name;
|
||||
|
||||
repository_handle
|
||||
.update(&mut cx, |repository_handle, _| {
|
||||
repository_handle.change_branch(branch_name)
|
||||
})?
|
||||
.await??;
|
||||
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn handle_show(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::GitShow>,
|
||||
@@ -566,6 +759,54 @@ impl GitStore {
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_askpass(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::AskPassRequest>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::AskPassResponse> {
|
||||
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
|
||||
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
|
||||
let repository =
|
||||
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
|
||||
|
||||
let delegates = cx.update(|cx| repository.read(cx).askpass_delegates.clone())?;
|
||||
let Some(mut askpass) = delegates.lock().remove(&envelope.payload.askpass_id) else {
|
||||
debug_panic!("no askpass found");
|
||||
return Err(anyhow::anyhow!("no askpass found"));
|
||||
};
|
||||
|
||||
let response = askpass.ask_password(envelope.payload.prompt).await?;
|
||||
|
||||
delegates
|
||||
.lock()
|
||||
.insert(envelope.payload.askpass_id, askpass);
|
||||
|
||||
Ok(proto::AskPassResponse { response })
|
||||
}
|
||||
|
||||
async fn handle_check_for_pushed_commits(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::CheckForPushedCommits>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::CheckForPushedCommitsResponse> {
|
||||
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
|
||||
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
|
||||
let repository_handle =
|
||||
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
|
||||
|
||||
let branches = repository_handle
|
||||
.update(&mut cx, |repository_handle, _| {
|
||||
repository_handle.check_for_pushed_commits()
|
||||
})?
|
||||
.await??;
|
||||
Ok(proto::CheckForPushedCommitsResponse {
|
||||
pushed_to: branches
|
||||
.into_iter()
|
||||
.map(|commit| commit.to_string())
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn repository_for_request(
|
||||
this: &Entity<Self>,
|
||||
worktree_id: WorktreeId,
|
||||
@@ -573,9 +814,8 @@ impl GitStore {
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Entity<Repository>> {
|
||||
this.update(cx, |this, cx| {
|
||||
let repository_handle = this
|
||||
.all_repositories()
|
||||
.into_iter()
|
||||
this.repositories
|
||||
.iter()
|
||||
.find(|repository_handle| {
|
||||
repository_handle.read(cx).worktree_id == worktree_id
|
||||
&& repository_handle
|
||||
@@ -584,12 +824,39 @@ impl GitStore {
|
||||
.work_directory_id()
|
||||
== work_directory_id
|
||||
})
|
||||
.context("missing repository handle")?;
|
||||
anyhow::Ok(repository_handle)
|
||||
.context("missing repository handle")
|
||||
.cloned()
|
||||
})?
|
||||
}
|
||||
}
|
||||
|
||||
fn make_remote_delegate(
|
||||
this: Entity<GitStore>,
|
||||
project_id: u64,
|
||||
worktree_id: WorktreeId,
|
||||
work_directory_id: ProjectEntryId,
|
||||
askpass_id: u64,
|
||||
cx: &mut AsyncApp,
|
||||
) -> AskPassDelegate {
|
||||
AskPassDelegate::new(cx, move |prompt, tx, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
let response = this.client.request(proto::AskPassRequest {
|
||||
project_id,
|
||||
worktree_id: worktree_id.to_proto(),
|
||||
work_directory_id: work_directory_id.to_proto(),
|
||||
askpass_id,
|
||||
prompt,
|
||||
});
|
||||
cx.spawn(|_, _| async move {
|
||||
tx.send(response.await?.response).ok();
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
impl GitRepo {}
|
||||
|
||||
impl Repository {
|
||||
@@ -691,6 +958,33 @@ impl Repository {
|
||||
self.worktree_id_path_to_repo_path(path.worktree_id, &path.path)
|
||||
}
|
||||
|
||||
// note: callers must verify these come from the same worktree
|
||||
pub fn contains_sub_repo(&self, other: &Entity<Self>, cx: &App) -> bool {
|
||||
let other_work_dir = &other.read(cx).repository_entry.work_directory;
|
||||
match (&self.repository_entry.work_directory, other_work_dir) {
|
||||
(WorkDirectory::InProject { .. }, WorkDirectory::AboveProject { .. }) => false,
|
||||
(WorkDirectory::AboveProject { .. }, WorkDirectory::InProject { .. }) => true,
|
||||
(
|
||||
WorkDirectory::InProject {
|
||||
relative_path: this_path,
|
||||
},
|
||||
WorkDirectory::InProject {
|
||||
relative_path: other_path,
|
||||
},
|
||||
) => other_path.starts_with(this_path),
|
||||
(
|
||||
WorkDirectory::AboveProject {
|
||||
absolute_path: this_path,
|
||||
..
|
||||
},
|
||||
WorkDirectory::AboveProject {
|
||||
absolute_path: other_path,
|
||||
..
|
||||
},
|
||||
) => other_path.starts_with(this_path),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn worktree_id_path_to_repo_path(
|
||||
&self,
|
||||
worktree_id: WorktreeId,
|
||||
@@ -1046,18 +1340,6 @@ impl Repository {
|
||||
self.repository_entry.status_len()
|
||||
}
|
||||
|
||||
fn have_changes(&self) -> bool {
|
||||
self.repository_entry.status_summary() != GitSummary::UNCHANGED
|
||||
}
|
||||
|
||||
fn have_staged_changes(&self) -> bool {
|
||||
self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
|
||||
}
|
||||
|
||||
pub fn can_commit(&self, commit_all: bool) -> bool {
|
||||
return self.have_changes() && (commit_all || self.have_staged_changes());
|
||||
}
|
||||
|
||||
pub fn commit(
|
||||
&self,
|
||||
message: SharedString,
|
||||
@@ -1096,21 +1378,39 @@ impl Repository {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn fetch(&self) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
|
||||
self.send_job(|git_repo| async move {
|
||||
pub fn fetch(
|
||||
&mut self,
|
||||
askpass: AskPassDelegate,
|
||||
cx: &App,
|
||||
) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
|
||||
let executor = cx.background_executor().clone();
|
||||
let askpass_delegates = self.askpass_delegates.clone();
|
||||
let askpass_id = util::post_inc(&mut self.latest_askpass_id);
|
||||
|
||||
self.send_job(move |git_repo| async move {
|
||||
match git_repo {
|
||||
GitRepo::Local(git_repository) => git_repository.fetch(),
|
||||
GitRepo::Local(git_repository) => {
|
||||
let askpass = AskPassSession::new(&executor, askpass).await?;
|
||||
git_repository.fetch(askpass)
|
||||
}
|
||||
GitRepo::Remote {
|
||||
project_id,
|
||||
client,
|
||||
worktree_id,
|
||||
work_directory_id,
|
||||
} => {
|
||||
askpass_delegates.lock().insert(askpass_id, askpass);
|
||||
let _defer = util::defer(|| {
|
||||
let askpass_delegate = askpass_delegates.lock().remove(&askpass_id);
|
||||
debug_assert!(askpass_delegate.is_some());
|
||||
});
|
||||
|
||||
let response = client
|
||||
.request(proto::Fetch {
|
||||
project_id: project_id.0,
|
||||
worktree_id: worktree_id.to_proto(),
|
||||
work_directory_id: work_directory_id.to_proto(),
|
||||
askpass_id,
|
||||
})
|
||||
.await
|
||||
.context("sending fetch request")?;
|
||||
@@ -1125,25 +1425,40 @@ impl Repository {
|
||||
}
|
||||
|
||||
pub fn push(
|
||||
&self,
|
||||
&mut self,
|
||||
branch: SharedString,
|
||||
remote: SharedString,
|
||||
options: Option<PushOptions>,
|
||||
askpass: AskPassDelegate,
|
||||
cx: &App,
|
||||
) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
|
||||
let executor = cx.background_executor().clone();
|
||||
let askpass_delegates = self.askpass_delegates.clone();
|
||||
let askpass_id = util::post_inc(&mut self.latest_askpass_id);
|
||||
|
||||
self.send_job(move |git_repo| async move {
|
||||
match git_repo {
|
||||
GitRepo::Local(git_repository) => git_repository.push(&branch, &remote, options),
|
||||
GitRepo::Local(git_repository) => {
|
||||
let askpass = AskPassSession::new(&executor, askpass).await?;
|
||||
git_repository.push(&branch, &remote, options, askpass)
|
||||
}
|
||||
GitRepo::Remote {
|
||||
project_id,
|
||||
client,
|
||||
worktree_id,
|
||||
work_directory_id,
|
||||
} => {
|
||||
askpass_delegates.lock().insert(askpass_id, askpass);
|
||||
let _defer = util::defer(|| {
|
||||
let askpass_delegate = askpass_delegates.lock().remove(&askpass_id);
|
||||
debug_assert!(askpass_delegate.is_some());
|
||||
});
|
||||
let response = client
|
||||
.request(proto::Push {
|
||||
project_id: project_id.0,
|
||||
worktree_id: worktree_id.to_proto(),
|
||||
work_directory_id: work_directory_id.to_proto(),
|
||||
askpass_id,
|
||||
branch_name: branch.to_string(),
|
||||
remote_name: remote.to_string(),
|
||||
options: options.map(|options| match options {
|
||||
@@ -1164,24 +1479,38 @@ impl Repository {
|
||||
}
|
||||
|
||||
pub fn pull(
|
||||
&self,
|
||||
&mut self,
|
||||
branch: SharedString,
|
||||
remote: SharedString,
|
||||
askpass: AskPassDelegate,
|
||||
cx: &App,
|
||||
) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
|
||||
self.send_job(|git_repo| async move {
|
||||
let executor = cx.background_executor().clone();
|
||||
let askpass_delegates = self.askpass_delegates.clone();
|
||||
let askpass_id = util::post_inc(&mut self.latest_askpass_id);
|
||||
self.send_job(move |git_repo| async move {
|
||||
match git_repo {
|
||||
GitRepo::Local(git_repository) => git_repository.pull(&branch, &remote),
|
||||
GitRepo::Local(git_repository) => {
|
||||
let askpass = AskPassSession::new(&executor, askpass).await?;
|
||||
git_repository.pull(&branch, &remote, askpass)
|
||||
}
|
||||
GitRepo::Remote {
|
||||
project_id,
|
||||
client,
|
||||
worktree_id,
|
||||
work_directory_id,
|
||||
} => {
|
||||
askpass_delegates.lock().insert(askpass_id, askpass);
|
||||
let _defer = util::defer(|| {
|
||||
let askpass_delegate = askpass_delegates.lock().remove(&askpass_id);
|
||||
debug_assert!(askpass_delegate.is_some());
|
||||
});
|
||||
let response = client
|
||||
.request(proto::Pull {
|
||||
project_id: project_id.0,
|
||||
worktree_id: worktree_id.to_proto(),
|
||||
work_directory_id: work_directory_id.to_proto(),
|
||||
askpass_id,
|
||||
branch_name: branch.to_string(),
|
||||
remote_name: remote.to_string(),
|
||||
})
|
||||
@@ -1197,7 +1526,7 @@ impl Repository {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_index_text(
|
||||
fn set_index_text(
|
||||
&self,
|
||||
path: &RepoPath,
|
||||
content: Option<String>,
|
||||
@@ -1267,4 +1596,110 @@ impl Repository {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn branches(&self) -> oneshot::Receiver<Result<Vec<Branch>>> {
|
||||
self.send_job(|repo| async move {
|
||||
match repo {
|
||||
GitRepo::Local(git_repository) => git_repository.branches(),
|
||||
GitRepo::Remote {
|
||||
project_id,
|
||||
client,
|
||||
worktree_id,
|
||||
work_directory_id,
|
||||
} => {
|
||||
let response = client
|
||||
.request(proto::GitGetBranches {
|
||||
project_id: project_id.0,
|
||||
worktree_id: worktree_id.to_proto(),
|
||||
work_directory_id: work_directory_id.to_proto(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let branches = response
|
||||
.branches
|
||||
.into_iter()
|
||||
.map(|branch| worktree::proto_to_branch(&branch))
|
||||
.collect();
|
||||
|
||||
Ok(branches)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_branch(&self, branch_name: String) -> oneshot::Receiver<Result<()>> {
|
||||
self.send_job(|repo| async move {
|
||||
match repo {
|
||||
GitRepo::Local(git_repository) => git_repository.create_branch(&branch_name),
|
||||
GitRepo::Remote {
|
||||
project_id,
|
||||
client,
|
||||
worktree_id,
|
||||
work_directory_id,
|
||||
} => {
|
||||
client
|
||||
.request(proto::GitCreateBranch {
|
||||
project_id: project_id.0,
|
||||
worktree_id: worktree_id.to_proto(),
|
||||
work_directory_id: work_directory_id.to_proto(),
|
||||
branch_name,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn change_branch(&self, branch_name: String) -> oneshot::Receiver<Result<()>> {
|
||||
self.send_job(|repo| async move {
|
||||
match repo {
|
||||
GitRepo::Local(git_repository) => git_repository.change_branch(&branch_name),
|
||||
GitRepo::Remote {
|
||||
project_id,
|
||||
client,
|
||||
worktree_id,
|
||||
work_directory_id,
|
||||
} => {
|
||||
client
|
||||
.request(proto::GitChangeBranch {
|
||||
project_id: project_id.0,
|
||||
worktree_id: worktree_id.to_proto(),
|
||||
work_directory_id: work_directory_id.to_proto(),
|
||||
branch_name,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn check_for_pushed_commits(&self) -> oneshot::Receiver<Result<Vec<SharedString>>> {
|
||||
self.send_job(|repo| async move {
|
||||
match repo {
|
||||
GitRepo::Local(git_repository) => git_repository.check_for_pushed_commit(),
|
||||
GitRepo::Remote {
|
||||
project_id,
|
||||
client,
|
||||
worktree_id,
|
||||
work_directory_id,
|
||||
} => {
|
||||
let response = client
|
||||
.request(proto::CheckForPushedCommits {
|
||||
project_id: project_id.0,
|
||||
worktree_id: worktree_id.to_proto(),
|
||||
work_directory_id: work_directory_id.to_proto(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let branches = response.pushed_to.into_iter().map(Into::into).collect();
|
||||
|
||||
Ok(branches)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3184,7 +3184,7 @@ impl LspStore {
|
||||
}
|
||||
}
|
||||
}
|
||||
BufferStoreEvent::BufferDropped(_) => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,11 +46,7 @@ use futures::{
|
||||
pub use image_store::{ImageItem, ImageStore};
|
||||
use image_store::{ImageItemEvent, ImageStoreEvent};
|
||||
|
||||
use ::git::{
|
||||
blame::Blame,
|
||||
repository::{Branch, GitRepository, RepoPath},
|
||||
status::FileStatus,
|
||||
};
|
||||
use ::git::{blame::Blame, repository::GitRepository, status::FileStatus};
|
||||
use gpui::{
|
||||
AnyEntity, App, AppContext as _, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter,
|
||||
Hsla, SharedString, Task, WeakEntity, Window,
|
||||
@@ -701,8 +697,15 @@ impl Project {
|
||||
)
|
||||
});
|
||||
|
||||
let git_store =
|
||||
cx.new(|cx| GitStore::new(&worktree_store, buffer_store.clone(), None, None, cx));
|
||||
let git_store = cx.new(|cx| {
|
||||
GitStore::new(
|
||||
&worktree_store,
|
||||
buffer_store.clone(),
|
||||
client.clone().into(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
|
||||
|
||||
@@ -826,7 +829,7 @@ impl Project {
|
||||
GitStore::new(
|
||||
&worktree_store,
|
||||
buffer_store.clone(),
|
||||
Some(ssh_proto.clone()),
|
||||
ssh_proto.clone(),
|
||||
Some(ProjectId(SSH_PROJECT_ID)),
|
||||
cx,
|
||||
)
|
||||
@@ -1034,7 +1037,7 @@ impl Project {
|
||||
GitStore::new(
|
||||
&worktree_store,
|
||||
buffer_store.clone(),
|
||||
Some(client.clone().into()),
|
||||
client.clone().into(),
|
||||
Some(ProjectId(remote_id)),
|
||||
cx,
|
||||
)
|
||||
@@ -2266,7 +2269,6 @@ impl Project {
|
||||
BufferStoreEvent::BufferAdded(buffer) => {
|
||||
self.register_buffer(buffer, cx).log_err();
|
||||
}
|
||||
BufferStoreEvent::BufferChangedFilePath { .. } => {}
|
||||
BufferStoreEvent::BufferDropped(buffer_id) => {
|
||||
if let Some(ref ssh_client) = self.ssh_client {
|
||||
ssh_client
|
||||
@@ -2279,6 +2281,7 @@ impl Project {
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3655,21 +3658,6 @@ impl Project {
|
||||
worktree.get_local_repo(&root_entry)?.repo().clone().into()
|
||||
}
|
||||
|
||||
pub fn branches(&self, project_path: ProjectPath, cx: &App) -> Task<Result<Vec<Branch>>> {
|
||||
self.worktree_store().read(cx).branches(project_path, cx)
|
||||
}
|
||||
|
||||
pub fn update_or_create_branch(
|
||||
&self,
|
||||
repository: ProjectPath,
|
||||
new_branch: String,
|
||||
cx: &App,
|
||||
) -> Task<Result<()>> {
|
||||
self.worktree_store()
|
||||
.read(cx)
|
||||
.update_or_create_branch(repository, new_branch, cx)
|
||||
}
|
||||
|
||||
pub fn blame_buffer(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
@@ -4301,25 +4289,8 @@ impl Project {
|
||||
self.git_store.read(cx).all_repositories()
|
||||
}
|
||||
|
||||
pub fn repository_and_path_for_buffer_id(
|
||||
&self,
|
||||
buffer_id: BufferId,
|
||||
cx: &App,
|
||||
) -> Option<(Entity<Repository>, RepoPath)> {
|
||||
let path = self
|
||||
.buffer_for_id(buffer_id, cx)?
|
||||
.read(cx)
|
||||
.project_path(cx)?;
|
||||
self.git_store
|
||||
.read(cx)
|
||||
.all_repositories()
|
||||
.into_iter()
|
||||
.find_map(|repo| {
|
||||
Some((
|
||||
repo.clone(),
|
||||
repo.read(cx).repository_entry.relativize(&path.path).ok()?,
|
||||
))
|
||||
})
|
||||
pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option<FileStatus> {
|
||||
self.git_store.read(cx).status_for_buffer_id(buffer_id, cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -168,6 +168,10 @@ pub struct GitSettings {
|
||||
///
|
||||
/// Default: on
|
||||
pub inline_blame: Option<InlineBlameSettings>,
|
||||
/// How hunks are displayed visually in the editor.
|
||||
///
|
||||
/// Default: transparent
|
||||
pub hunk_style: Option<GitHunkStyleSetting>,
|
||||
}
|
||||
|
||||
impl GitSettings {
|
||||
@@ -200,6 +204,16 @@ impl GitSettings {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GitHunkStyleSetting {
|
||||
/// Show unstaged hunks with a transparent background
|
||||
#[default]
|
||||
Transparent,
|
||||
/// Show unstaged hunks with a pattern background
|
||||
Pattern,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GitGutterSetting {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::{task_inventory::TaskContexts, Event, *};
|
||||
use buffer_diff::{assert_hunks, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
|
||||
use buffer_diff::{
|
||||
assert_hunks, BufferDiffEvent, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind,
|
||||
};
|
||||
use fs::FakeFs;
|
||||
use futures::{future, StreamExt};
|
||||
use gpui::{App, SemanticVersion, UpdateGlobal};
|
||||
@@ -5786,7 +5788,7 @@ async fn test_unstaged_diff_for_buffer(cx: &mut gpui::TestAppContext) {
|
||||
unstaged_diff.update(cx, |unstaged_diff, cx| {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
assert_hunks(
|
||||
unstaged_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
|
||||
unstaged_diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&unstaged_diff.base_text_string().unwrap(),
|
||||
&[
|
||||
@@ -6008,6 +6010,271 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
|
||||
use DiffHunkSecondaryStatus::*;
|
||||
init_test(cx);
|
||||
|
||||
let committed_contents = r#"
|
||||
zero
|
||||
one
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
"#
|
||||
.unindent();
|
||||
let file_contents = r#"
|
||||
one
|
||||
TWO
|
||||
three
|
||||
FOUR
|
||||
five
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
".git": {},
|
||||
"file.txt": file_contents.clone()
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
fs.set_head_for_repo(
|
||||
"/dir/.git".as_ref(),
|
||||
&[("file.txt".into(), committed_contents.clone())],
|
||||
);
|
||||
fs.set_index_for_repo(
|
||||
"/dir/.git".as_ref(),
|
||||
&[("file.txt".into(), committed_contents.clone())],
|
||||
);
|
||||
|
||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/dir/file.txt", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
let uncommitted_diff = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_uncommitted_diff(buffer.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let mut diff_events = cx.events(&uncommitted_diff);
|
||||
|
||||
// The hunks are initially unstaged.
|
||||
uncommitted_diff.read_with(cx, |diff, cx| {
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&[
|
||||
(
|
||||
0..0,
|
||||
"zero\n",
|
||||
"",
|
||||
DiffHunkStatus::deleted(HasSecondaryHunk),
|
||||
),
|
||||
(
|
||||
1..2,
|
||||
"two\n",
|
||||
"TWO\n",
|
||||
DiffHunkStatus::modified(HasSecondaryHunk),
|
||||
),
|
||||
(
|
||||
3..4,
|
||||
"four\n",
|
||||
"FOUR\n",
|
||||
DiffHunkStatus::modified(HasSecondaryHunk),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
// Stage a hunk. It appears as optimistically staged.
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
let range =
|
||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_before(Point::new(2, 0));
|
||||
let hunks = diff
|
||||
.hunks_intersecting_range(range, &snapshot, cx)
|
||||
.collect::<Vec<_>>();
|
||||
diff.stage_or_unstage_hunks(true, &hunks, &snapshot, true, cx);
|
||||
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&[
|
||||
(
|
||||
0..0,
|
||||
"zero\n",
|
||||
"",
|
||||
DiffHunkStatus::deleted(HasSecondaryHunk),
|
||||
),
|
||||
(
|
||||
1..2,
|
||||
"two\n",
|
||||
"TWO\n",
|
||||
DiffHunkStatus::modified(SecondaryHunkRemovalPending),
|
||||
),
|
||||
(
|
||||
3..4,
|
||||
"four\n",
|
||||
"FOUR\n",
|
||||
DiffHunkStatus::modified(HasSecondaryHunk),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
// The diff emits a change event for the range of the staged hunk.
|
||||
assert!(matches!(
|
||||
diff_events.next().await.unwrap(),
|
||||
BufferDiffEvent::HunksStagedOrUnstaged(_)
|
||||
));
|
||||
let event = diff_events.next().await.unwrap();
|
||||
if let BufferDiffEvent::DiffChanged {
|
||||
changed_range: Some(changed_range),
|
||||
} = event
|
||||
{
|
||||
let changed_range = changed_range.to_point(&snapshot);
|
||||
assert_eq!(changed_range, Point::new(1, 0)..Point::new(2, 0));
|
||||
} else {
|
||||
panic!("Unexpected event {event:?}");
|
||||
}
|
||||
|
||||
// When the write to the index completes, it appears as staged.
|
||||
cx.run_until_parked();
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&[
|
||||
(
|
||||
0..0,
|
||||
"zero\n",
|
||||
"",
|
||||
DiffHunkStatus::deleted(HasSecondaryHunk),
|
||||
),
|
||||
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)),
|
||||
(
|
||||
3..4,
|
||||
"four\n",
|
||||
"FOUR\n",
|
||||
DiffHunkStatus::modified(HasSecondaryHunk),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
// The diff emits a change event for the changed index text.
|
||||
let event = diff_events.next().await.unwrap();
|
||||
if let BufferDiffEvent::DiffChanged {
|
||||
changed_range: Some(changed_range),
|
||||
} = event
|
||||
{
|
||||
let changed_range = changed_range.to_point(&snapshot);
|
||||
assert_eq!(changed_range, Point::new(0, 0)..Point::new(5, 0));
|
||||
} else {
|
||||
panic!("Unexpected event {event:?}");
|
||||
}
|
||||
|
||||
// Simulate a problem writing to the git index.
|
||||
fs.set_error_message_for_index_write(
|
||||
"/dir/.git".as_ref(),
|
||||
Some("failed to write git index".into()),
|
||||
);
|
||||
|
||||
// Stage another hunk.
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
let range =
|
||||
snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_before(Point::new(4, 0));
|
||||
let hunks = diff
|
||||
.hunks_intersecting_range(range, &snapshot, cx)
|
||||
.collect::<Vec<_>>();
|
||||
diff.stage_or_unstage_hunks(true, &hunks, &snapshot, true, cx);
|
||||
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&[
|
||||
(
|
||||
0..0,
|
||||
"zero\n",
|
||||
"",
|
||||
DiffHunkStatus::deleted(HasSecondaryHunk),
|
||||
),
|
||||
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)),
|
||||
(
|
||||
3..4,
|
||||
"four\n",
|
||||
"FOUR\n",
|
||||
DiffHunkStatus::modified(SecondaryHunkRemovalPending),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
assert!(matches!(
|
||||
diff_events.next().await.unwrap(),
|
||||
BufferDiffEvent::HunksStagedOrUnstaged(_)
|
||||
));
|
||||
let event = diff_events.next().await.unwrap();
|
||||
if let BufferDiffEvent::DiffChanged {
|
||||
changed_range: Some(changed_range),
|
||||
} = event
|
||||
{
|
||||
let changed_range = changed_range.to_point(&snapshot);
|
||||
assert_eq!(changed_range, Point::new(3, 0)..Point::new(4, 0));
|
||||
} else {
|
||||
panic!("Unexpected event {event:?}");
|
||||
}
|
||||
|
||||
// When the write fails, the hunk returns to being unstaged.
|
||||
cx.run_until_parked();
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&[
|
||||
(
|
||||
0..0,
|
||||
"zero\n",
|
||||
"",
|
||||
DiffHunkStatus::deleted(HasSecondaryHunk),
|
||||
),
|
||||
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)),
|
||||
(
|
||||
3..4,
|
||||
"four\n",
|
||||
"FOUR\n",
|
||||
DiffHunkStatus::modified(HasSecondaryHunk),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
let event = diff_events.next().await.unwrap();
|
||||
if let BufferDiffEvent::DiffChanged {
|
||||
changed_range: Some(changed_range),
|
||||
} = event
|
||||
{
|
||||
let changed_range = changed_range.to_point(&snapshot);
|
||||
assert_eq!(changed_range, Point::new(0, 0)..Point::new(5, 0));
|
||||
} else {
|
||||
panic!("Unexpected event {event:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -6065,7 +6332,7 @@ async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) {
|
||||
uncommitted_diff.update(cx, |uncommitted_diff, cx| {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
assert_hunks(
|
||||
uncommitted_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
|
||||
uncommitted_diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&uncommitted_diff.base_text_string().unwrap(),
|
||||
&[(
|
||||
|
||||
@@ -27,10 +27,7 @@ use smol::{
|
||||
};
|
||||
use text::ReplicaId;
|
||||
use util::{paths::SanitizedPath, ResultExt};
|
||||
use worktree::{
|
||||
branch_to_proto, Entry, ProjectEntryId, UpdatedEntriesSet, Worktree, WorktreeId,
|
||||
WorktreeSettings,
|
||||
};
|
||||
use worktree::{Entry, ProjectEntryId, UpdatedEntriesSet, Worktree, WorktreeId, WorktreeSettings};
|
||||
|
||||
use crate::{search::SearchQuery, ProjectPath};
|
||||
|
||||
@@ -83,8 +80,6 @@ impl WorktreeStore {
|
||||
client.add_entity_request_handler(Self::handle_delete_project_entry);
|
||||
client.add_entity_request_handler(Self::handle_expand_project_entry);
|
||||
client.add_entity_request_handler(Self::handle_expand_all_for_project_entry);
|
||||
client.add_entity_request_handler(Self::handle_git_branches);
|
||||
client.add_entity_request_handler(Self::handle_update_branch);
|
||||
}
|
||||
|
||||
pub fn local(retain_worktrees: bool, fs: Arc<dyn Fs>) -> Self {
|
||||
@@ -890,150 +885,6 @@ impl WorktreeStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn branches(
|
||||
&self,
|
||||
project_path: ProjectPath,
|
||||
cx: &App,
|
||||
) -> Task<Result<Vec<git::repository::Branch>>> {
|
||||
let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) else {
|
||||
return Task::ready(Err(anyhow!("No worktree found for ProjectPath")));
|
||||
};
|
||||
|
||||
match worktree.read(cx) {
|
||||
Worktree::Local(local_worktree) => {
|
||||
let branches = util::maybe!({
|
||||
let worktree_error = |error| {
|
||||
format!(
|
||||
"{} for worktree {}",
|
||||
error,
|
||||
local_worktree.abs_path().to_string_lossy()
|
||||
)
|
||||
};
|
||||
|
||||
let entry = local_worktree
|
||||
.git_entry(project_path.path)
|
||||
.with_context(|| worktree_error("No git entry found"))?;
|
||||
|
||||
let repo = local_worktree
|
||||
.get_local_repo(&entry)
|
||||
.with_context(|| worktree_error("No repository found"))?
|
||||
.repo()
|
||||
.clone();
|
||||
|
||||
repo.branches()
|
||||
});
|
||||
|
||||
Task::ready(branches)
|
||||
}
|
||||
Worktree::Remote(remote_worktree) => {
|
||||
let request = remote_worktree.client().request(proto::GitBranches {
|
||||
project_id: remote_worktree.project_id(),
|
||||
repository: Some(proto::ProjectPath {
|
||||
worktree_id: project_path.worktree_id.to_proto(),
|
||||
path: project_path.path.to_proto(), // Root path
|
||||
}),
|
||||
});
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let response = request.await?;
|
||||
|
||||
let branches = response
|
||||
.branches
|
||||
.into_iter()
|
||||
.map(|proto_branch| git::repository::Branch {
|
||||
is_head: proto_branch.is_head,
|
||||
name: proto_branch.name.into(),
|
||||
upstream: proto_branch.upstream.map(|upstream| {
|
||||
git::repository::Upstream {
|
||||
ref_name: upstream.ref_name.into(),
|
||||
tracking: upstream
|
||||
.tracking
|
||||
.map(|tracking| {
|
||||
git::repository::UpstreamTracking::Tracked(
|
||||
git::repository::UpstreamTrackingStatus {
|
||||
ahead: tracking.ahead as u32,
|
||||
behind: tracking.behind as u32,
|
||||
},
|
||||
)
|
||||
})
|
||||
.unwrap_or(git::repository::UpstreamTracking::Gone),
|
||||
}
|
||||
}),
|
||||
most_recent_commit: proto_branch.most_recent_commit.map(|commit| {
|
||||
git::repository::CommitSummary {
|
||||
sha: commit.sha.into(),
|
||||
subject: commit.subject.into(),
|
||||
commit_timestamp: commit.commit_timestamp,
|
||||
}
|
||||
}),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(branches)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_or_create_branch(
|
||||
&self,
|
||||
repository: ProjectPath,
|
||||
new_branch: String,
|
||||
cx: &App,
|
||||
) -> Task<Result<()>> {
|
||||
let Some(worktree) = self.worktree_for_id(repository.worktree_id, cx) else {
|
||||
return Task::ready(Err(anyhow!("No worktree found for ProjectPath")));
|
||||
};
|
||||
|
||||
match worktree.read(cx) {
|
||||
Worktree::Local(local_worktree) => {
|
||||
let result = util::maybe!({
|
||||
let worktree_error = |error| {
|
||||
format!(
|
||||
"{} for worktree {}",
|
||||
error,
|
||||
local_worktree.abs_path().to_string_lossy()
|
||||
)
|
||||
};
|
||||
|
||||
let entry = local_worktree
|
||||
.git_entry(repository.path)
|
||||
.with_context(|| worktree_error("No git entry found"))?;
|
||||
|
||||
let repo = local_worktree
|
||||
.get_local_repo(&entry)
|
||||
.with_context(|| worktree_error("No repository found"))?
|
||||
.repo()
|
||||
.clone();
|
||||
|
||||
if !repo.branch_exits(&new_branch)? {
|
||||
repo.create_branch(&new_branch)?;
|
||||
}
|
||||
|
||||
repo.change_branch(&new_branch)?;
|
||||
Ok(())
|
||||
});
|
||||
|
||||
Task::ready(result)
|
||||
}
|
||||
Worktree::Remote(remote_worktree) => {
|
||||
let request = remote_worktree.client().request(proto::UpdateGitBranch {
|
||||
project_id: remote_worktree.project_id(),
|
||||
repository: Some(proto::ProjectPath {
|
||||
worktree_id: repository.worktree_id.to_proto(),
|
||||
path: repository.path.to_proto(), // Root path
|
||||
}),
|
||||
branch_name: new_branch,
|
||||
});
|
||||
|
||||
cx.background_spawn(async move {
|
||||
request.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn filter_paths(
|
||||
fs: &Arc<dyn Fs>,
|
||||
mut input: Receiver<MatchingEntry>,
|
||||
@@ -1130,54 +981,6 @@ impl WorktreeStore {
|
||||
.ok_or_else(|| anyhow!("invalid request"))?;
|
||||
Worktree::handle_expand_all_for_entry(worktree, envelope.payload, cx).await
|
||||
}
|
||||
|
||||
pub async fn handle_git_branches(
|
||||
this: Entity<Self>,
|
||||
branches: TypedEnvelope<proto::GitBranches>,
|
||||
cx: AsyncApp,
|
||||
) -> Result<proto::GitBranchesResponse> {
|
||||
let project_path = branches
|
||||
.payload
|
||||
.repository
|
||||
.clone()
|
||||
.context("Invalid GitBranches call")?;
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: WorktreeId::from_proto(project_path.worktree_id),
|
||||
path: Arc::<Path>::from_proto(project_path.path),
|
||||
};
|
||||
|
||||
let branches = this
|
||||
.read_with(&cx, |this, cx| this.branches(project_path, cx))?
|
||||
.await?;
|
||||
|
||||
Ok(proto::GitBranchesResponse {
|
||||
branches: branches.iter().map(branch_to_proto).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn handle_update_branch(
|
||||
this: Entity<Self>,
|
||||
update_branch: TypedEnvelope<proto::UpdateGitBranch>,
|
||||
cx: AsyncApp,
|
||||
) -> Result<proto::Ack> {
|
||||
let project_path = update_branch
|
||||
.payload
|
||||
.repository
|
||||
.clone()
|
||||
.context("Invalid GitBranches call")?;
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: WorktreeId::from_proto(project_path.worktree_id),
|
||||
path: Arc::<Path>::from_proto(project_path.path),
|
||||
};
|
||||
let new_branch = update_branch.payload.branch_name;
|
||||
|
||||
this.read_with(&cx, |this, cx| {
|
||||
this.update_or_create_branch(project_path, new_branch, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
syntax = "proto3";
|
||||
package zed.messages;
|
||||
|
||||
@@ -278,7 +279,6 @@ message Envelope {
|
||||
LanguageServerPromptRequest language_server_prompt_request = 268;
|
||||
LanguageServerPromptResponse language_server_prompt_response = 269;
|
||||
|
||||
GitBranches git_branches = 270;
|
||||
GitBranchesResponse git_branches_response = 271;
|
||||
|
||||
UpdateGitBranch update_git_branch = 272;
|
||||
@@ -332,7 +332,17 @@ message Envelope {
|
||||
ApplyCodeActionKind apply_code_action_kind = 309;
|
||||
ApplyCodeActionKindResponse apply_code_action_kind_response = 310;
|
||||
|
||||
RemoteMessageResponse remote_message_response = 311; // current max
|
||||
RemoteMessageResponse remote_message_response = 311;
|
||||
|
||||
GitGetBranches git_get_branches = 312;
|
||||
GitCreateBranch git_create_branch = 313;
|
||||
GitChangeBranch git_change_branch = 314;
|
||||
|
||||
CheckForPushedCommits check_for_pushed_commits = 315;
|
||||
CheckForPushedCommitsResponse check_for_pushed_commits_response = 316;
|
||||
|
||||
AskPassRequest ask_pass_request = 317;
|
||||
AskPassResponse ask_pass_response = 318; // current max
|
||||
}
|
||||
|
||||
reserved 87 to 88;
|
||||
@@ -348,6 +358,7 @@ message Envelope {
|
||||
reserved 221;
|
||||
reserved 224 to 229;
|
||||
reserved 246;
|
||||
reserved 270;
|
||||
reserved 247 to 254;
|
||||
reserved 255 to 256;
|
||||
}
|
||||
@@ -2801,6 +2812,7 @@ message Push {
|
||||
string remote_name = 4;
|
||||
string branch_name = 5;
|
||||
optional PushOptions options = 6;
|
||||
uint64 askpass_id = 7;
|
||||
|
||||
enum PushOptions {
|
||||
SET_UPSTREAM = 0;
|
||||
@@ -2812,6 +2824,7 @@ message Fetch {
|
||||
uint64 project_id = 1;
|
||||
uint64 worktree_id = 2;
|
||||
uint64 work_directory_id = 3;
|
||||
uint64 askpass_id = 4;
|
||||
}
|
||||
|
||||
message GetRemotes {
|
||||
@@ -2835,9 +2848,52 @@ message Pull {
|
||||
uint64 work_directory_id = 3;
|
||||
string remote_name = 4;
|
||||
string branch_name = 5;
|
||||
uint64 askpass_id = 6;
|
||||
}
|
||||
|
||||
message RemoteMessageResponse {
|
||||
string stdout = 1;
|
||||
string stderr = 2;
|
||||
}
|
||||
|
||||
message AskPassRequest {
|
||||
uint64 project_id = 1;
|
||||
uint64 worktree_id = 2;
|
||||
uint64 work_directory_id = 3;
|
||||
uint64 askpass_id = 4;
|
||||
string prompt = 5;
|
||||
}
|
||||
|
||||
message AskPassResponse {
|
||||
string response = 1;
|
||||
}
|
||||
|
||||
message GitGetBranches {
|
||||
uint64 project_id = 1;
|
||||
uint64 worktree_id = 2;
|
||||
uint64 work_directory_id = 3;
|
||||
}
|
||||
|
||||
message GitCreateBranch {
|
||||
uint64 project_id = 1;
|
||||
uint64 worktree_id = 2;
|
||||
uint64 work_directory_id = 3;
|
||||
string branch_name = 4;
|
||||
}
|
||||
|
||||
message GitChangeBranch {
|
||||
uint64 project_id = 1;
|
||||
uint64 worktree_id = 2;
|
||||
uint64 work_directory_id = 3;
|
||||
string branch_name = 4;
|
||||
}
|
||||
|
||||
message CheckForPushedCommits {
|
||||
uint64 project_id = 1;
|
||||
uint64 worktree_id = 2;
|
||||
uint64 work_directory_id = 3;
|
||||
}
|
||||
|
||||
message CheckForPushedCommitsResponse {
|
||||
repeated string pushed_to = 1;
|
||||
}
|
||||
|
||||
@@ -424,7 +424,7 @@ messages!(
|
||||
(FlushBufferedMessages, Foreground),
|
||||
(LanguageServerPromptRequest, Foreground),
|
||||
(LanguageServerPromptResponse, Foreground),
|
||||
(GitBranches, Background),
|
||||
(GitGetBranches, Background),
|
||||
(GitBranchesResponse, Background),
|
||||
(UpdateGitBranch, Background),
|
||||
(ListToolchains, Foreground),
|
||||
@@ -452,6 +452,12 @@ messages!(
|
||||
(GetRemotesResponse, Background),
|
||||
(Pull, Background),
|
||||
(RemoteMessageResponse, Background),
|
||||
(AskPassRequest, Background),
|
||||
(AskPassResponse, Background),
|
||||
(GitCreateBranch, Background),
|
||||
(GitChangeBranch, Background),
|
||||
(CheckForPushedCommits, Background),
|
||||
(CheckForPushedCommitsResponse, Background),
|
||||
);
|
||||
|
||||
request_messages!(
|
||||
@@ -575,7 +581,7 @@ request_messages!(
|
||||
(GetPermalinkToLine, GetPermalinkToLineResponse),
|
||||
(FlushBufferedMessages, Ack),
|
||||
(LanguageServerPromptRequest, LanguageServerPromptResponse),
|
||||
(GitBranches, GitBranchesResponse),
|
||||
(GitGetBranches, GitBranchesResponse),
|
||||
(UpdateGitBranch, Ack),
|
||||
(ListToolchains, ListToolchainsResponse),
|
||||
(ActivateToolchain, Ack),
|
||||
@@ -594,6 +600,10 @@ request_messages!(
|
||||
(Fetch, RemoteMessageResponse),
|
||||
(GetRemotes, GetRemotesResponse),
|
||||
(Pull, RemoteMessageResponse),
|
||||
(AskPassRequest, AskPassResponse),
|
||||
(GitCreateBranch, Ack),
|
||||
(GitChangeBranch, Ack),
|
||||
(CheckForPushedCommits, CheckForPushedCommitsResponse),
|
||||
);
|
||||
|
||||
entity_messages!(
|
||||
@@ -679,7 +689,7 @@ entity_messages!(
|
||||
OpenServerSettings,
|
||||
GetPermalinkToLine,
|
||||
LanguageServerPromptRequest,
|
||||
GitBranches,
|
||||
GitGetBranches,
|
||||
UpdateGitBranch,
|
||||
ListToolchains,
|
||||
ActivateToolchain,
|
||||
@@ -695,6 +705,10 @@ entity_messages!(
|
||||
Fetch,
|
||||
GetRemotes,
|
||||
Pull,
|
||||
AskPassRequest,
|
||||
GitChangeBranch,
|
||||
GitCreateBranch,
|
||||
CheckForPushedCommits,
|
||||
);
|
||||
|
||||
entity_messages!(
|
||||
|
||||
@@ -131,7 +131,7 @@ pub struct SshPrompt {
|
||||
connection_string: SharedString,
|
||||
nickname: Option<SharedString>,
|
||||
status_message: Option<SharedString>,
|
||||
prompt: Option<(Entity<Markdown>, oneshot::Sender<Result<String>>)>,
|
||||
prompt: Option<(Entity<Markdown>, oneshot::Sender<String>)>,
|
||||
cancellation: Option<oneshot::Sender<()>>,
|
||||
editor: Entity<Editor>,
|
||||
}
|
||||
@@ -176,7 +176,7 @@ impl SshPrompt {
|
||||
pub fn set_prompt(
|
||||
&mut self,
|
||||
prompt: String,
|
||||
tx: oneshot::Sender<Result<String>>,
|
||||
tx: oneshot::Sender<String>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -223,7 +223,7 @@ impl SshPrompt {
|
||||
if let Some((_, tx)) = self.prompt.take() {
|
||||
self.status_message = Some("Connecting".into());
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
tx.send(Ok(editor.text(cx))).ok();
|
||||
tx.send(editor.text(cx)).ok();
|
||||
editor.clear(window, cx);
|
||||
});
|
||||
}
|
||||
@@ -429,11 +429,10 @@ pub struct SshClientDelegate {
|
||||
}
|
||||
|
||||
impl remote::SshClientDelegate for SshClientDelegate {
|
||||
fn ask_password(&self, prompt: String, cx: &mut AsyncApp) -> oneshot::Receiver<Result<String>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
fn ask_password(&self, prompt: String, tx: oneshot::Sender<String>, cx: &mut AsyncApp) {
|
||||
let mut known_password = self.known_password.clone();
|
||||
if let Some(password) = known_password.take() {
|
||||
tx.send(Ok(password)).ok();
|
||||
tx.send(password).ok();
|
||||
} else {
|
||||
self.window
|
||||
.update(cx, |_, window, cx| {
|
||||
@@ -443,7 +442,6 @@ impl remote::SshClientDelegate for SshClientDelegate {
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
rx
|
||||
}
|
||||
|
||||
fn set_status(&self, status: Option<&str>, cx: &mut AsyncApp) {
|
||||
|
||||
@@ -19,6 +19,7 @@ test-support = ["fs/test-support"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
askpass.workspace = true
|
||||
async-trait.workspace = true
|
||||
collections.workspace = true
|
||||
fs.workspace = true
|
||||
@@ -26,9 +27,10 @@ futures.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
log.workspace = true
|
||||
paths.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
prost.workspace = true
|
||||
release_channel.workspace = true
|
||||
rpc = { workspace = true, features = ["gpui"] }
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
@@ -38,8 +40,6 @@ smol.workspace = true
|
||||
tempfile.workspace = true
|
||||
thiserror.workspace = true
|
||||
util.workspace = true
|
||||
release_channel.workspace = true
|
||||
which.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -316,7 +316,7 @@ impl SshPlatform {
|
||||
}
|
||||
|
||||
pub trait SshClientDelegate: Send + Sync {
|
||||
fn ask_password(&self, prompt: String, cx: &mut AsyncApp) -> oneshot::Receiver<Result<String>>;
|
||||
fn ask_password(&self, prompt: String, tx: oneshot::Sender<String>, cx: &mut AsyncApp);
|
||||
fn get_download_params(
|
||||
&self,
|
||||
platform: SshPlatform,
|
||||
@@ -1454,83 +1454,22 @@ impl SshRemoteConnection {
|
||||
delegate: Arc<dyn SshClientDelegate>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Self> {
|
||||
use futures::AsyncWriteExt as _;
|
||||
use futures::{io::BufReader, AsyncBufReadExt as _};
|
||||
use smol::net::unix::UnixStream;
|
||||
use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener};
|
||||
use util::ResultExt as _;
|
||||
use askpass::AskPassResult;
|
||||
|
||||
delegate.set_status(Some("Connecting"), cx);
|
||||
|
||||
let url = connection_options.ssh_url();
|
||||
|
||||
let temp_dir = tempfile::Builder::new()
|
||||
.prefix("zed-ssh-session")
|
||||
.tempdir()?;
|
||||
|
||||
// Create a domain socket listener to handle requests from the askpass program.
|
||||
let askpass_socket = temp_dir.path().join("askpass.sock");
|
||||
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
|
||||
let listener =
|
||||
UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?;
|
||||
|
||||
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<UnixStream>();
|
||||
let mut kill_tx = Some(askpass_kill_master_tx);
|
||||
|
||||
let askpass_task = cx.spawn({
|
||||
let askpass_delegate = askpass::AskPassDelegate::new(cx, {
|
||||
let delegate = delegate.clone();
|
||||
|mut cx| async move {
|
||||
let mut askpass_opened_tx = Some(askpass_opened_tx);
|
||||
|
||||
while let Ok((mut stream, _)) = listener.accept().await {
|
||||
if let Some(askpass_opened_tx) = askpass_opened_tx.take() {
|
||||
askpass_opened_tx.send(()).ok();
|
||||
}
|
||||
let mut buffer = Vec::new();
|
||||
let mut reader = BufReader::new(&mut stream);
|
||||
if reader.read_until(b'\0', &mut buffer).await.is_err() {
|
||||
buffer.clear();
|
||||
}
|
||||
let password_prompt = String::from_utf8_lossy(&buffer);
|
||||
if let Some(password) = delegate
|
||||
.ask_password(password_prompt.to_string(), &mut cx)
|
||||
.await
|
||||
.context("failed to get ssh password")
|
||||
.and_then(|p| p)
|
||||
.log_err()
|
||||
{
|
||||
stream.write_all(password.as_bytes()).await.log_err();
|
||||
} else {
|
||||
if let Some(kill_tx) = kill_tx.take() {
|
||||
kill_tx.send(stream).log_err();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
move |prompt, tx, cx| delegate.ask_password(prompt, tx, cx)
|
||||
});
|
||||
|
||||
anyhow::ensure!(
|
||||
which::which("nc").is_ok(),
|
||||
"Cannot find `nc` command (netcat), which is required to connect over SSH."
|
||||
);
|
||||
|
||||
// Create an askpass script that communicates back to this process.
|
||||
let askpass_script = format!(
|
||||
"{shebang}\n{print_args} | {nc} -U {askpass_socket} 2> /dev/null \n",
|
||||
// on macOS `brew install netcat` provides the GNU netcat implementation
|
||||
// which does not support -U.
|
||||
nc = if cfg!(target_os = "macos") {
|
||||
"/usr/bin/nc"
|
||||
} else {
|
||||
"nc"
|
||||
},
|
||||
askpass_socket = askpass_socket.display(),
|
||||
print_args = "printf '%s\\0' \"$@\"",
|
||||
shebang = "#!/bin/sh",
|
||||
);
|
||||
let askpass_script_path = temp_dir.path().join("askpass.sh");
|
||||
fs::write(&askpass_script_path, askpass_script).await?;
|
||||
fs::set_permissions(&askpass_script_path, std::fs::Permissions::from_mode(0o755)).await?;
|
||||
let mut askpass =
|
||||
askpass::AskPassSession::new(cx.background_executor(), askpass_delegate).await?;
|
||||
|
||||
// Start the master SSH process, which does not do anything except for establish
|
||||
// the connection and keep it open, allowing other ssh commands to reuse it
|
||||
@@ -1542,7 +1481,7 @@ impl SshRemoteConnection {
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.env("SSH_ASKPASS_REQUIRE", "force")
|
||||
.env("SSH_ASKPASS", &askpass_script_path)
|
||||
.env("SSH_ASKPASS", &askpass.script_path())
|
||||
.args(connection_options.additional_args())
|
||||
.args([
|
||||
"-N",
|
||||
@@ -1556,35 +1495,25 @@ impl SshRemoteConnection {
|
||||
.arg(&url)
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
// Wait for this ssh process to close its stdout, indicating that authentication
|
||||
// has completed.
|
||||
let mut stdout = master_process.stdout.take().unwrap();
|
||||
let mut output = Vec::new();
|
||||
let connection_timeout = Duration::from_secs(10);
|
||||
|
||||
let result = select_biased! {
|
||||
_ = askpass_opened_rx.fuse() => {
|
||||
select_biased! {
|
||||
stream = askpass_kill_master_rx.fuse() => {
|
||||
result = askpass.run().fuse() => {
|
||||
match result {
|
||||
AskPassResult::CancelledByUser => {
|
||||
master_process.kill().ok();
|
||||
drop(stream);
|
||||
Err(anyhow!("SSH connection canceled"))
|
||||
Err(anyhow!("SSH connection canceled"))?
|
||||
}
|
||||
// If the askpass script has opened, that means the user is typing
|
||||
// their password, in which case we don't want to timeout anymore,
|
||||
// since we know a connection has been established.
|
||||
result = stdout.read_to_end(&mut output).fuse() => {
|
||||
result?;
|
||||
Ok(())
|
||||
AskPassResult::Timedout => {
|
||||
Err(anyhow!("connecting to host timed out"))?
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = stdout.read_to_end(&mut output).fuse() => {
|
||||
Ok(())
|
||||
}
|
||||
_ = futures::FutureExt::fuse(smol::Timer::after(connection_timeout)) => {
|
||||
Err(anyhow!("Exceeded {:?} timeout trying to connect to host", connection_timeout))
|
||||
anyhow::Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1592,8 +1521,6 @@ impl SshRemoteConnection {
|
||||
return Err(e.context("Failed to connect to host"));
|
||||
}
|
||||
|
||||
drop(askpass_task);
|
||||
|
||||
if master_process.try_status()?.is_some() {
|
||||
output.clear();
|
||||
let mut stderr = master_process.stderr.take().unwrap();
|
||||
@@ -1606,6 +1533,8 @@ impl SshRemoteConnection {
|
||||
Err(anyhow!(error_message))?;
|
||||
}
|
||||
|
||||
drop(askpass);
|
||||
|
||||
let socket = SshSocket {
|
||||
connection_options,
|
||||
socket_path,
|
||||
@@ -2558,7 +2487,7 @@ mod fake {
|
||||
pub(super) struct Delegate;
|
||||
|
||||
impl SshClientDelegate for Delegate {
|
||||
fn ask_password(&self, _: String, _: &mut AsyncApp) -> oneshot::Receiver<Result<String>> {
|
||||
fn ask_password(&self, _: String, _: oneshot::Sender<String>, _: &mut AsyncApp) {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
|
||||
@@ -87,8 +87,15 @@ impl HeadlessProject {
|
||||
buffer_store
|
||||
});
|
||||
|
||||
let git_store =
|
||||
cx.new(|cx| GitStore::new(&worktree_store, buffer_store.clone(), None, None, cx));
|
||||
let git_store = cx.new(|cx| {
|
||||
GitStore::new(
|
||||
&worktree_store,
|
||||
buffer_store.clone(),
|
||||
session.clone().into(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let prettier_store = cx.new(|cx| {
|
||||
PrettierStore::new(
|
||||
node_runtime.clone(),
|
||||
|
||||
@@ -1328,9 +1328,12 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA
|
||||
// Give the worktree a bit of time to index the file system
|
||||
cx.run_until_parked();
|
||||
|
||||
let remote_branches = project
|
||||
.update(cx, |project, cx| project.branches(root_path.clone(), cx))
|
||||
let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
|
||||
|
||||
let remote_branches = repository
|
||||
.update(cx, |repository, _| repository.branches())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let new_branch = branches[2];
|
||||
@@ -1342,13 +1345,10 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA
|
||||
|
||||
assert_eq!(&remote_branches, &branches_set);
|
||||
|
||||
cx.update(|cx| {
|
||||
project.update(cx, |project, cx| {
|
||||
project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| repository.read(cx).change_branch(new_branch.to_string()))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -1368,11 +1368,21 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA
|
||||
|
||||
// Also try creating a new branch
|
||||
cx.update(|cx| {
|
||||
project.update(cx, |project, cx| {
|
||||
project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
|
||||
})
|
||||
repository
|
||||
.read(cx)
|
||||
.create_branch("totally-new-branch".to_string())
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
repository
|
||||
.read(cx)
|
||||
.change_branch("totally-new-branch".to_string())
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -70,6 +70,10 @@ impl<K: Clone + Ord, V: Clone> TreeMap<K, V> {
|
||||
self.0.insert_or_replace(MapEntry { key, value }, &());
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.0 = SumTree::default();
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, key: &K) -> Option<V> {
|
||||
let mut removed = None;
|
||||
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(&());
|
||||
@@ -157,6 +161,14 @@ impl<K: Clone + Ord, V: Clone> TreeMap<K, V> {
|
||||
self.0.iter().map(|entry| &entry.value)
|
||||
}
|
||||
|
||||
pub fn first(&self) -> Option<(&K, &V)> {
|
||||
self.0.first().map(|entry| (&entry.key, &entry.value))
|
||||
}
|
||||
|
||||
pub fn last(&self) -> Option<(&K, &V)> {
|
||||
self.0.last().map(|entry| (&entry.key, &entry.value))
|
||||
}
|
||||
|
||||
pub fn insert_tree(&mut self, other: TreeMap<K, V>) {
|
||||
let edits = other
|
||||
.iter()
|
||||
|
||||
@@ -401,7 +401,7 @@ impl TitleBar {
|
||||
.child(
|
||||
Label::new(nickname.clone())
|
||||
.size(LabelSize::Small)
|
||||
.text_ellipsis(),
|
||||
.truncate(),
|
||||
),
|
||||
)
|
||||
.tooltip(move |window, cx| {
|
||||
|
||||
@@ -19,7 +19,6 @@ mod modal;
|
||||
mod navigable;
|
||||
mod numeric_stepper;
|
||||
mod popover;
|
||||
mod popover_button;
|
||||
mod popover_menu;
|
||||
mod radio;
|
||||
mod right_click_menu;
|
||||
@@ -57,7 +56,6 @@ pub use modal::*;
|
||||
pub use navigable::*;
|
||||
pub use numeric_stepper::*;
|
||||
pub use popover::*;
|
||||
pub use popover_button::*;
|
||||
pub use popover_menu::*;
|
||||
pub use radio::*;
|
||||
pub use right_click_menu::*;
|
||||
|
||||
@@ -97,6 +97,7 @@ pub struct Button {
|
||||
key_binding: Option<KeyBinding>,
|
||||
key_binding_position: KeybindingPosition,
|
||||
alpha: Option<f32>,
|
||||
truncate: bool,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
@@ -123,6 +124,7 @@ impl Button {
|
||||
key_binding: None,
|
||||
key_binding_position: KeybindingPosition::default(),
|
||||
alpha: None,
|
||||
truncate: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +208,15 @@ impl Button {
|
||||
self.alpha = Some(alpha);
|
||||
self
|
||||
}
|
||||
|
||||
/// Truncates overflowing labels with an ellipsis (`…`) if needed.
|
||||
///
|
||||
/// Buttons with static labels should _never_ be truncated, ensure
|
||||
/// this is only used when the label is dynamic and may overflow.
|
||||
pub fn truncate(mut self, truncate: bool) -> Self {
|
||||
self.truncate = truncate;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Toggleable for Button {
|
||||
@@ -437,7 +448,8 @@ impl RenderOnce for Button {
|
||||
.color(label_color)
|
||||
.size(self.label_size.unwrap_or_default())
|
||||
.when_some(self.alpha, |this, alpha| this.alpha(alpha))
|
||||
.line_height_style(LineHeightStyle::UiLabel),
|
||||
.line_height_style(LineHeightStyle::UiLabel)
|
||||
.when(self.truncate, |this| this.truncate()),
|
||||
)
|
||||
.children(self.key_binding),
|
||||
)
|
||||
|
||||
@@ -64,8 +64,8 @@ impl LabelCommon for HighlightedLabel {
|
||||
self
|
||||
}
|
||||
|
||||
fn text_ellipsis(mut self) -> Self {
|
||||
self.base = self.base.text_ellipsis();
|
||||
fn truncate(mut self) -> Self {
|
||||
self.base = self.base.truncate();
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
@@ -171,8 +171,9 @@ impl LabelCommon for Label {
|
||||
self
|
||||
}
|
||||
|
||||
fn text_ellipsis(mut self) -> Self {
|
||||
self.base = self.base.text_ellipsis();
|
||||
/// Truncates overflowing text with an ellipsis (`…`) if needed.
|
||||
fn truncate(mut self) -> Self {
|
||||
self.base = self.base.truncate();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -240,7 +241,7 @@ mod label_preview {
|
||||
"Special Cases",
|
||||
vec![
|
||||
single_example("Single Line", Label::new("Line 1\nLine 2\nLine 3").single_line().into_any_element()),
|
||||
single_example("Text Ellipsis", div().max_w_24().child(Label::new("This is a very long file name that should be truncated: very_long_file_name_with_many_words.rs").text_ellipsis()).into_any_element()),
|
||||
single_example("Text Ellipsis", div().max_w_24().child(Label::new("This is a very long file name that should be truncated: very_long_file_name_with_many_words.rs").truncate()).into_any_element()),
|
||||
],
|
||||
),
|
||||
])
|
||||
|
||||
@@ -57,7 +57,7 @@ pub trait LabelCommon {
|
||||
fn alpha(self, alpha: f32) -> Self;
|
||||
|
||||
/// Truncates overflowing text with an ellipsis (`…`) if needed.
|
||||
fn text_ellipsis(self) -> Self;
|
||||
fn truncate(self) -> Self;
|
||||
|
||||
/// Sets the label to render as a single line.
|
||||
fn single_line(self) -> Self;
|
||||
@@ -84,7 +84,7 @@ pub struct LabelLike {
|
||||
alpha: Option<f32>,
|
||||
underline: bool,
|
||||
single_line: bool,
|
||||
text_ellipsis: bool,
|
||||
truncate: bool,
|
||||
}
|
||||
|
||||
impl Default for LabelLike {
|
||||
@@ -109,7 +109,7 @@ impl LabelLike {
|
||||
alpha: None,
|
||||
underline: false,
|
||||
single_line: false,
|
||||
text_ellipsis: false,
|
||||
truncate: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,8 +166,9 @@ impl LabelCommon for LabelLike {
|
||||
self
|
||||
}
|
||||
|
||||
fn text_ellipsis(mut self) -> Self {
|
||||
self.text_ellipsis = true;
|
||||
/// Truncates overflowing text with an ellipsis (`…`) if needed.
|
||||
fn truncate(mut self) -> Self {
|
||||
self.truncate = true;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -220,7 +221,7 @@ impl RenderOnce for LabelLike {
|
||||
})
|
||||
.when(self.strikethrough, |this| this.line_through())
|
||||
.when(self.single_line, |this| this.whitespace_nowrap())
|
||||
.when(self.text_ellipsis, |this| {
|
||||
.when(self.truncate, |this| {
|
||||
this.overflow_x_hidden().text_ellipsis()
|
||||
})
|
||||
.text_color(color)
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
use gpui::{AnyView, Corner, Entity, ManagedView};
|
||||
|
||||
use crate::{prelude::*, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
|
||||
|
||||
pub trait TriggerablePopover: ManagedView {
|
||||
fn menu_handle(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> PopoverMenuHandle<Self>;
|
||||
}
|
||||
|
||||
pub struct PopoverButton<T, B, F> {
|
||||
selector: Entity<T>,
|
||||
button: B,
|
||||
tooltip: F,
|
||||
corner: Corner,
|
||||
}
|
||||
|
||||
impl<T, B, F> PopoverButton<T, B, F> {
|
||||
pub fn new(selector: Entity<T>, corner: Corner, button: B, tooltip: F) -> Self
|
||||
where
|
||||
F: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
{
|
||||
Self {
|
||||
selector,
|
||||
button,
|
||||
tooltip,
|
||||
corner,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: TriggerablePopover, B: PopoverTrigger + ButtonCommon, F> RenderOnce
|
||||
for PopoverButton<T, B, F>
|
||||
where
|
||||
F: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
{
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let menu_handle = self
|
||||
.selector
|
||||
.update(cx, |selector, cx| selector.menu_handle(window, cx));
|
||||
|
||||
PopoverMenu::new("popover-button")
|
||||
.menu({
|
||||
let selector = self.selector.clone();
|
||||
move |_window, _cx| Some(selector.clone())
|
||||
})
|
||||
.trigger_with_tooltip(self.button, self.tooltip)
|
||||
.anchor(self.corner)
|
||||
.with_handle(menu_handle)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-2.0),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@ git_ui.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
parking_lot.workspace = true
|
||||
project_panel.workspace = true
|
||||
|
||||
@@ -1329,12 +1329,25 @@ pub(crate) fn start_of_relative_buffer_row(
|
||||
|
||||
fn up_down_buffer_rows(
|
||||
map: &DisplaySnapshot,
|
||||
point: DisplayPoint,
|
||||
mut point: DisplayPoint,
|
||||
mut goal: SelectionGoal,
|
||||
times: isize,
|
||||
mut times: isize,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let bias = if times < 0 { Bias::Left } else { Bias::Right };
|
||||
|
||||
while map.is_folded_buffer_header(point.row()) {
|
||||
if times < 0 {
|
||||
(point, _) = movement::up(map, point, goal, true, text_layout_details);
|
||||
times += 1;
|
||||
} else if times > 0 {
|
||||
(point, _) = movement::down(map, point, goal, true, text_layout_details);
|
||||
times -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let start = map.display_point_to_fold_point(point, Bias::Left);
|
||||
let begin_folded_line = map.fold_point_to_display_point(
|
||||
map.fold_snapshot
|
||||
|
||||
@@ -6,9 +6,13 @@ use std::time::Duration;
|
||||
|
||||
use collections::HashMap;
|
||||
use command_palette::CommandPalette;
|
||||
use editor::{actions::DeleteLine, display_map::DisplayRow, DisplayPoint};
|
||||
use editor::{
|
||||
actions::DeleteLine, display_map::DisplayRow, test::editor_test_context::EditorTestContext,
|
||||
DisplayPoint, Editor, EditorMode, MultiBuffer,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use gpui::{KeyBinding, Modifiers, MouseButton, TestAppContext};
|
||||
use language::Point;
|
||||
pub use neovim_backed_test_context::*;
|
||||
use settings::SettingsStore;
|
||||
pub use vim_test_context::*;
|
||||
@@ -1707,3 +1711,202 @@ async fn test_ctrl_o_dot(cx: &mut gpui::TestAppContext) {
|
||||
cx.simulate_shared_keystrokes("l l escape .").await;
|
||||
cx.shared_state().await.assert_eq("hellˇllo world.");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_folded_multibuffer_excerpts(cx: &mut gpui::TestAppContext) {
|
||||
VimTestContext::init(cx);
|
||||
cx.update(|cx| {
|
||||
VimTestContext::init_keybindings(true, cx);
|
||||
});
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| {
|
||||
let multi_buffer = MultiBuffer::build_multi(
|
||||
[
|
||||
("111\n222\n333\n444\n", vec![Point::row_range(0..2)]),
|
||||
("aaa\nbbb\nccc\nddd\n", vec![Point::row_range(0..2)]),
|
||||
("AAA\nBBB\nCCC\nDDD\n", vec![Point::row_range(0..2)]),
|
||||
("one\ntwo\nthr\nfou\n", vec![Point::row_range(0..2)]),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
let mut editor = Editor::new(
|
||||
EditorMode::Full,
|
||||
multi_buffer.clone(),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
|
||||
// fold all but the second buffer, so that we test navigating between two
|
||||
// adjacent folded buffers, as well as folded buffers at the start and
|
||||
// end the multibuffer
|
||||
editor.fold_buffer(buffer_ids[0], cx);
|
||||
editor.fold_buffer(buffer_ids[2], cx);
|
||||
editor.fold_buffer(buffer_ids[3], cx);
|
||||
|
||||
editor
|
||||
});
|
||||
let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
|
||||
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
ˇ[FOLDED]
|
||||
[EXCERPT]
|
||||
aaa
|
||||
bbb
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
"
|
||||
});
|
||||
cx.simulate_keystroke("j");
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
ˇaaa
|
||||
bbb
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
"
|
||||
});
|
||||
cx.simulate_keystroke("j");
|
||||
cx.simulate_keystroke("j");
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
aaa
|
||||
bbb
|
||||
ˇ[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
"
|
||||
});
|
||||
cx.simulate_keystroke("j");
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
aaa
|
||||
bbb
|
||||
[EXCERPT]
|
||||
ˇ[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
"
|
||||
});
|
||||
cx.simulate_keystroke("j");
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
aaa
|
||||
bbb
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
ˇ[FOLDED]
|
||||
"
|
||||
});
|
||||
cx.simulate_keystroke("k");
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
aaa
|
||||
bbb
|
||||
[EXCERPT]
|
||||
ˇ[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
"
|
||||
});
|
||||
cx.simulate_keystroke("k");
|
||||
cx.simulate_keystroke("k");
|
||||
cx.simulate_keystroke("k");
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
ˇaaa
|
||||
bbb
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
"
|
||||
});
|
||||
cx.simulate_keystroke("k");
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
ˇ[FOLDED]
|
||||
[EXCERPT]
|
||||
aaa
|
||||
bbb
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
"
|
||||
});
|
||||
cx.simulate_keystroke("shift-g");
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
aaa
|
||||
bbb
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
ˇ[FOLDED]
|
||||
"
|
||||
});
|
||||
cx.simulate_keystrokes("g g");
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
ˇ[FOLDED]
|
||||
[EXCERPT]
|
||||
aaa
|
||||
bbb
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
"
|
||||
});
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
|
||||
editor.fold_buffer(buffer_ids[1], cx);
|
||||
});
|
||||
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
ˇ[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
"
|
||||
});
|
||||
cx.simulate_keystrokes("2 j");
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
ˇ[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ impl VimTestContext {
|
||||
git_ui::init(cx);
|
||||
crate::init(cx);
|
||||
search::init(cx);
|
||||
language::init(cx);
|
||||
editor::init_settings(cx);
|
||||
project::Project::init_settings(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,22 +60,26 @@ impl VimTestContext {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn init_keybindings(enabled: bool, cx: &mut App) {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
|
||||
});
|
||||
let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
|
||||
"keymaps/default-macos.json",
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
cx.bind_keys(default_key_bindings);
|
||||
if enabled {
|
||||
let vim_key_bindings =
|
||||
settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
|
||||
cx.bind_keys(vim_key_bindings);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_lsp(mut cx: EditorLspTestContext, enabled: bool) -> VimTestContext {
|
||||
cx.update(|_, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
|
||||
});
|
||||
let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
|
||||
"keymaps/default-macos.json",
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
cx.bind_keys(default_key_bindings);
|
||||
if enabled {
|
||||
let vim_key_bindings =
|
||||
settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
|
||||
cx.bind_keys(vim_key_bindings);
|
||||
}
|
||||
Self::init_keybindings(enabled, cx);
|
||||
});
|
||||
|
||||
// Setup search toolbars and keypress hook
|
||||
|
||||
@@ -4292,7 +4292,11 @@ impl BackgroundScanner {
|
||||
let mut containing_git_repository = None;
|
||||
for (index, ancestor) in root_abs_path.as_path().ancestors().enumerate() {
|
||||
if index != 0 {
|
||||
if let Ok(ignore) =
|
||||
if Some(ancestor) == self.fs.home_dir().as_deref() {
|
||||
// Unless $HOME is itself the worktree root, don't consider it as a
|
||||
// containing git repository---expensive and likely unwanted.
|
||||
break;
|
||||
} else if let Ok(ignore) =
|
||||
build_gitignore(&ancestor.join(*GITIGNORE), self.fs.as_ref()).await
|
||||
{
|
||||
self.state
|
||||
@@ -4304,6 +4308,7 @@ impl BackgroundScanner {
|
||||
}
|
||||
|
||||
let ancestor_dot_git = ancestor.join(*DOT_GIT);
|
||||
log::debug!("considering ancestor: {ancestor_dot_git:?}");
|
||||
// Check whether the directory or file called `.git` exists (in the
|
||||
// case of worktrees it's a file.)
|
||||
if self
|
||||
@@ -4312,21 +4317,26 @@ impl BackgroundScanner {
|
||||
.await
|
||||
.is_ok_and(|metadata| metadata.is_some())
|
||||
{
|
||||
log::debug!(".git path exists");
|
||||
if index != 0 {
|
||||
// We canonicalize, since the FS events use the canonicalized path.
|
||||
if let Some(ancestor_dot_git) =
|
||||
self.fs.canonicalize(&ancestor_dot_git).await.log_err()
|
||||
{
|
||||
let location_in_repo = root_abs_path
|
||||
.as_path()
|
||||
.strip_prefix(ancestor)
|
||||
.unwrap()
|
||||
.into();
|
||||
log::debug!(
|
||||
"inserting parent git repo for this worktree: {location_in_repo:?}"
|
||||
);
|
||||
// We associate the external git repo with our root folder and
|
||||
// also mark where in the git repo the root folder is located.
|
||||
let local_repository = self.state.lock().insert_git_repository_for_path(
|
||||
WorkDirectory::AboveProject {
|
||||
absolute_path: ancestor.into(),
|
||||
location_in_repo: root_abs_path
|
||||
.as_path()
|
||||
.strip_prefix(ancestor)
|
||||
.unwrap()
|
||||
.into(),
|
||||
location_in_repo,
|
||||
},
|
||||
ancestor_dot_git.clone().into(),
|
||||
self.fs.as_ref(),
|
||||
@@ -4341,9 +4351,13 @@ impl BackgroundScanner {
|
||||
|
||||
// Reached root of git repository.
|
||||
break;
|
||||
} else {
|
||||
log::debug!(".git path doesn't exist");
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("containing git repository: {containing_git_repository:?}");
|
||||
|
||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||
{
|
||||
let mut state = self.state.lock();
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::{
|
||||
use anyhow::Result;
|
||||
use fs::{FakeFs, Fs, RealFs, RemoveOptions};
|
||||
use git::{
|
||||
repository::RepoPath,
|
||||
status::{
|
||||
FileStatus, GitSummary, StatusCode, TrackedStatus, TrackedSummary, UnmergedStatus,
|
||||
UnmergedStatusCode,
|
||||
@@ -2241,6 +2242,73 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_home_dir_as_git_repository(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"home": {
|
||||
".git": {},
|
||||
"project": {
|
||||
"a.txt": "A"
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
fs.set_home_dir(Path::new(path!("/root/home")).to_owned());
|
||||
|
||||
let tree = Worktree::local(
|
||||
Path::new(path!("/root/home/project")),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let tree = tree.as_local().unwrap();
|
||||
|
||||
let repo = tree.repository_for_path(path!("a.txt").as_ref());
|
||||
assert!(repo.is_none());
|
||||
});
|
||||
|
||||
let home_tree = Worktree::local(
|
||||
Path::new(path!("/root/home")),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.read(|cx| home_tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
home_tree.flush_fs_events(cx).await;
|
||||
|
||||
home_tree.read_with(cx, |home_tree, _cx| {
|
||||
let home_tree = home_tree.as_local().unwrap();
|
||||
|
||||
let repo = home_tree.repository_for_path(path!("project/a.txt").as_ref());
|
||||
assert_eq!(
|
||||
repo.map(|repo| &repo.work_directory),
|
||||
Some(&WorkDirectory::InProject {
|
||||
relative_path: Path::new("").into()
|
||||
})
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_git_repository_for_path(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -3240,6 +3308,87 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_conflicted_cherry_pick(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let root = TempTree::new(json!({
|
||||
"project": {
|
||||
"a.txt": "a",
|
||||
},
|
||||
}));
|
||||
let root_path = root.path();
|
||||
|
||||
let tree = Worktree::local(
|
||||
root_path,
|
||||
true,
|
||||
Arc::new(RealFs::default()),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let repo = git_init(&root_path.join("project"));
|
||||
git_add("a.txt", &repo);
|
||||
git_commit("init", &repo);
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
git_branch("other-branch", &repo);
|
||||
git_checkout("refs/heads/other-branch", &repo);
|
||||
std::fs::write(root_path.join("project/a.txt"), "A").unwrap();
|
||||
git_add("a.txt", &repo);
|
||||
git_commit("capitalize", &repo);
|
||||
let commit = repo
|
||||
.head()
|
||||
.expect("Failed to get HEAD")
|
||||
.peel_to_commit()
|
||||
.expect("HEAD is not a commit");
|
||||
git_checkout("refs/heads/master", &repo);
|
||||
std::fs::write(root_path.join("project/a.txt"), "b").unwrap();
|
||||
git_add("a.txt", &repo);
|
||||
git_commit("improve letter", &repo);
|
||||
git_cherry_pick(&commit, &repo);
|
||||
std::fs::read_to_string(root_path.join("project/.git/CHERRY_PICK_HEAD"))
|
||||
.expect("No CHERRY_PICK_HEAD");
|
||||
pretty_assertions::assert_eq!(
|
||||
git_status(&repo),
|
||||
collections::HashMap::from_iter([("a.txt".to_owned(), git2::Status::CONFLICTED)])
|
||||
);
|
||||
tree.flush_fs_events(cx).await;
|
||||
let conflicts = tree.update(cx, |tree, _| {
|
||||
let entry = tree.git_entries().nth(0).expect("No git entry").clone();
|
||||
entry
|
||||
.current_merge_conflicts
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
pretty_assertions::assert_eq!(conflicts, [RepoPath::from("a.txt")]);
|
||||
|
||||
git_add("a.txt", &repo);
|
||||
// Attempt to manually simulate what `git cherry-pick --continue` would do.
|
||||
git_commit("whatevs", &repo);
|
||||
std::fs::remove_file(root.path().join("project/.git/CHERRY_PICK_HEAD"))
|
||||
.expect("Failed to remove CHERRY_PICK_HEAD");
|
||||
pretty_assertions::assert_eq!(git_status(&repo), collections::HashMap::default());
|
||||
tree.flush_fs_events(cx).await;
|
||||
let conflicts = tree.update(cx, |tree, _| {
|
||||
let entry = tree.git_entries().nth(0).expect("No git entry").clone();
|
||||
entry
|
||||
.current_merge_conflicts
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
pretty_assertions::assert_eq!(conflicts, []);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -3338,6 +3487,11 @@ fn git_commit(msg: &'static str, repo: &git2::Repository) {
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_cherry_pick(commit: &git2::Commit<'_>, repo: &git2::Repository) {
|
||||
repo.cherrypick(commit, None).expect("Failed to cherrypick");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_stash(repo: &mut git2::Repository) {
|
||||
use git2::Signature;
|
||||
@@ -3363,6 +3517,22 @@ fn git_reset(offset: usize, repo: &git2::Repository) {
|
||||
.expect("Could not reset");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_branch(name: &str, repo: &git2::Repository) {
|
||||
let head = repo
|
||||
.head()
|
||||
.expect("Couldn't get repo head")
|
||||
.peel_to_commit()
|
||||
.expect("HEAD is not a commit");
|
||||
repo.branch(name, &head, false).expect("Failed to commit");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_checkout(name: &str, repo: &git2::Repository) {
|
||||
repo.set_head(name).expect("Failed to set head");
|
||||
repo.checkout_head(None).expect("Failed to check out head");
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[track_caller]
|
||||
fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.177.0"
|
||||
version = "0.177.4"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
preview
|
||||
@@ -507,7 +507,6 @@ fn main() {
|
||||
outline::init(cx);
|
||||
project_symbols::init(cx);
|
||||
project_panel::init(cx);
|
||||
git_ui::git_panel::init(cx);
|
||||
outline_panel::init(cx);
|
||||
component_preview::init(cx);
|
||||
tasks_ui::init(cx);
|
||||
@@ -1020,7 +1019,7 @@ fn eager_load_active_theme_and_icon_theme(fs: Arc<dyn Fs>, cx: &App) {
|
||||
let extension_store = ExtensionStore::global(cx);
|
||||
let theme_registry = ThemeRegistry::global(cx);
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
let appearance = cx.window_appearance().into();
|
||||
let appearance = SystemAppearance::global(cx).0;
|
||||
|
||||
if let Some(theme_selection) = theme_settings.theme_selection.as_ref() {
|
||||
let theme_name = theme_selection.theme(appearance);
|
||||
|
||||
@@ -182,18 +182,8 @@ impl Render for QuickActionBar {
|
||||
.action("Next Problem", Box::new(GoToDiagnostic))
|
||||
.action("Previous Problem", Box::new(GoToPreviousDiagnostic))
|
||||
.separator()
|
||||
.action(
|
||||
"Next Hunk",
|
||||
Box::new(GoToHunk {
|
||||
center_cursor: true,
|
||||
}),
|
||||
)
|
||||
.action(
|
||||
"Previous Hunk",
|
||||
Box::new(GoToPreviousHunk {
|
||||
center_cursor: true,
|
||||
}),
|
||||
)
|
||||
.action("Next Hunk", Box::new(GoToHunk))
|
||||
.action("Previous Hunk", Box::new(GoToPreviousHunk))
|
||||
.separator()
|
||||
.action("Move Line Up", Box::new(MoveLineUp))
|
||||
.action("Move Line Down", Box::new(MoveLineDown))
|
||||
|
||||
@@ -10,6 +10,12 @@ To preview the docs locally you will need to install [mdBook](https://rust-lang.
|
||||
mdbook serve docs
|
||||
```
|
||||
|
||||
Before committing, verify that the docs are formatted in the way prettier expects with:
|
||||
|
||||
```
|
||||
cd docs && pnpm dlx prettier@3.5.0 . --write && cd ..
|
||||
```
|
||||
|
||||
## Preprocessor
|
||||
|
||||
We have a custom mdbook preprocessor for interfacing with our crates (`crates/docs_preprocessor`).
|
||||
|
||||
@@ -72,10 +72,15 @@ The following commands use the language server to help you navigate and refactor
|
||||
|
||||
### Git
|
||||
|
||||
| Command | Default Shortcut |
|
||||
| ------------------------- | ---------------- |
|
||||
| Go to next git change | `] c` |
|
||||
| Go to previous git change | `[ c` |
|
||||
| Command | Default Shortcut |
|
||||
| ------------------------------- | ---------------- |
|
||||
| Go to next git change | `] c` |
|
||||
| Go to previous git change | `[ c` |
|
||||
| Expand diff hunk | `d o` |
|
||||
| Toggle staged | `d O` |
|
||||
| Stage and next (in diff view) | `d u` |
|
||||
| Unstage and next (in diff view) | `d U` |
|
||||
| Restore change | `d p` |
|
||||
|
||||
### Treesitter
|
||||
|
||||
|
||||
Reference in New Issue
Block a user