Compare commits
38 Commits
cnext-cpre
...
v0.177.3-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
135
Cargo.lock
generated
135
Cargo.lock
generated
@@ -1178,9 +1178,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 +1271,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 +1295,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 +1317,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 +1351,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 +1373,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 +1395,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 +1458,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 +1810,7 @@ dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.10.5",
|
||||
"itertools 0.12.1",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
"log",
|
||||
@@ -1833,7 +1833,7 @@ dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.10.5",
|
||||
"itertools 0.12.1",
|
||||
"log",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
@@ -2404,6 +2404,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 +2520,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 +2530,7 @@ dependencies = [
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3508,10 +3527,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",
|
||||
]
|
||||
|
||||
@@ -5400,8 +5420,10 @@ dependencies = [
|
||||
"buffer_diff",
|
||||
"collections",
|
||||
"component",
|
||||
"ctor",
|
||||
"db",
|
||||
"editor",
|
||||
"env_logger 0.11.6",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
@@ -5563,7 +5585,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"calloop",
|
||||
"calloop-wayland-source",
|
||||
"cbindgen",
|
||||
"cbindgen 0.28.0",
|
||||
"cocoa 0.26.0",
|
||||
"collections",
|
||||
"core-foundation 0.9.4",
|
||||
@@ -7236,9 +7258,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 +9787,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"
|
||||
@@ -10413,7 +10426,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 +10459,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",
|
||||
@@ -11474,9 +11487,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 +11498,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 +11511,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 +11788,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 +11801,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 +11866,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 +11894,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 +14246,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 +14816,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 +14920,7 @@ dependencies = [
|
||||
"multi_buffer",
|
||||
"nvim-rs",
|
||||
"parking_lot",
|
||||
"project",
|
||||
"project_panel",
|
||||
"regex",
|
||||
"release_channel",
|
||||
@@ -15929,12 +15942,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 +16777,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.177.0"
|
||||
version = "0.177.3"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
||||
@@ -538,7 +538,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"
|
||||
}
|
||||
@@ -722,7 +722,7 @@
|
||||
"tab": "git_panel::FocusEditor",
|
||||
"shift-tab": "git_panel::FocusEditor",
|
||||
"escape": "git_panel::ToggleFocus",
|
||||
"ctrl-enter": "git::Commit",
|
||||
"ctrl-enter": "git::ShowCommitEditor",
|
||||
"alt-enter": "menu::SecondaryConfirm"
|
||||
}
|
||||
},
|
||||
@@ -736,7 +736,7 @@
|
||||
{
|
||||
"context": "GitDiff > Editor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "git::Commit"
|
||||
"ctrl-enter": "git::ShowCommitEditor"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -749,14 +749,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"
|
||||
}
|
||||
@@ -743,14 +743,14 @@
|
||||
"tab": "git_panel::FocusEditor",
|
||||
"shift-tab": "git_panel::FocusEditor",
|
||||
"escape": "git_panel::ToggleFocus",
|
||||
"cmd-enter": "git::Commit"
|
||||
"cmd-enter": "git::ShowCommitEditor"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitDiff > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "git::Commit"
|
||||
"cmd-enter": "git::ShowCommitEditor"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -1136,12 +1132,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 +1144,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);
|
||||
|
||||
@@ -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<_>>(),
|
||||
|
||||
@@ -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,43 @@ 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();
|
||||
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();
|
||||
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>,
|
||||
@@ -11412,14 +11449,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 +11465,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 +11506,7 @@ impl Editor {
|
||||
|
||||
fn go_to_prev_hunk(
|
||||
&mut self,
|
||||
action: &GoToPreviousHunk,
|
||||
_: &GoToPreviousHunk,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -11485,8 +11515,7 @@ impl Editor {
|
||||
self.go_to_hunk_after_or_before_position(
|
||||
&snapshot,
|
||||
selection.head(),
|
||||
false,
|
||||
action.center_cursor,
|
||||
Direction::Prev,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -11496,7 +11525,7 @@ impl Editor {
|
||||
&mut self,
|
||||
snapshot: &EditorSnapshot,
|
||||
position: Point,
|
||||
) -> Option<MultiBufferDiffHunk> {
|
||||
) -> Option<MultiBufferRow> {
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.diff_hunk_before(position)
|
||||
@@ -12965,13 +12994,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);
|
||||
@@ -13558,20 +13592,20 @@ impl Editor {
|
||||
|
||||
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(
|
||||
@@ -13593,102 +13627,33 @@ impl Editor {
|
||||
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);
|
||||
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.go_to_next_hunk(&GoToHunk, window, cx);
|
||||
}
|
||||
|
||||
fn do_stage_or_unstage(
|
||||
@@ -13728,7 +13693,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,
|
||||
@@ -16036,9 +16001,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,
|
||||
);
|
||||
@@ -17156,17 +17121,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::{
|
||||
@@ -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,
|
||||
);
|
||||
@@ -2722,7 +2724,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
|
||||
@@ -4343,7 +4348,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 +4418,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 +4558,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 +5689,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 +5701,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 +6736,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 +6756,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 +7233,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 +7282,7 @@ impl Element for EditorElement {
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
&rows_with_hunk_bounds,
|
||||
&display_hunks,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -7283,7 +7310,7 @@ impl Element for EditorElement {
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
&rows_with_hunk_bounds,
|
||||
&display_hunks,
|
||||
&snapshot,
|
||||
window,
|
||||
cx,
|
||||
@@ -8873,7 +8900,7 @@ fn diff_hunk_controls(
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Next Hunk",
|
||||
&GoToHunk::default(),
|
||||
&GoToHunk,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
@@ -8888,7 +8915,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 +8936,7 @@ fn diff_hunk_controls(
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Previous Hunk",
|
||||
&GoToPreviousHunk::default(),
|
||||
&GoToPreviousHunk,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
@@ -8920,7 +8951,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,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1524,6 +1531,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 +2090,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> {
|
||||
|
||||
@@ -36,23 +36,15 @@ 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]);
|
||||
impl_actions!(git, [Push]);
|
||||
|
||||
actions!(
|
||||
git,
|
||||
[
|
||||
// per-hunk
|
||||
ToggleStaged,
|
||||
StageAndNext,
|
||||
UnstageAndNext,
|
||||
// per-file
|
||||
StageFile,
|
||||
UnstageFile,
|
||||
|
||||
@@ -57,6 +57,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"] }
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use 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 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};
|
||||
@@ -31,35 +29,16 @@ pub fn open(
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let project = workspace.project().clone();
|
||||
let this = cx.entity();
|
||||
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(project, 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> {
|
||||
cx.new(|cx| {
|
||||
let mut list = BranchList::new(project, BranchListStyle::Popover, 15., cx);
|
||||
list.reload_branches(window, cx);
|
||||
let list = BranchList::new(project, BranchListStyle::Popover, 15., window, cx);
|
||||
list.focus_handle(cx).focus(window);
|
||||
list
|
||||
})
|
||||
}
|
||||
@@ -72,59 +51,54 @@ 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(
|
||||
project_handle: Entity<Project>,
|
||||
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 project = project_handle.read(cx);
|
||||
let all_branches_request = project
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.map(|worktree| project.branches(ProjectPath::root_path(worktree.read(cx).id()), cx))
|
||||
.context("No worktrees found");
|
||||
|
||||
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?.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(project_handle.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 +106,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 +114,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,7 +144,7 @@ impl BranchEntry {
|
||||
|
||||
pub struct BranchListDelegate {
|
||||
matches: Vec<BranchEntry>,
|
||||
all_branches: Vec<Branch>,
|
||||
all_branches: Option<Vec<Branch>>,
|
||||
project: Entity<Project>,
|
||||
style: BranchListStyle,
|
||||
selected_index: usize,
|
||||
@@ -194,33 +154,20 @@ pub struct BranchListDelegate {
|
||||
}
|
||||
|
||||
impl BranchListDelegate {
|
||||
async fn new(
|
||||
fn new(
|
||||
project: Entity<Project>,
|
||||
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,
|
||||
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 +208,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()
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::git_panel::{commit_message_editor, GitPanel};
|
||||
use git::{Commit, ShowCommitEditor};
|
||||
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::*;
|
||||
@@ -115,27 +115,9 @@ impl CommitModal {
|
||||
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)
|
||||
git_panel.update(cx, |git_panel, cx| {
|
||||
git_panel.set_modal_open(true, cx);
|
||||
});
|
||||
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();
|
||||
@@ -163,30 +145,30 @@ impl CommitModal {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let panel = git_panel.read(cx);
|
||||
let suggested_message = panel.suggest_commit_message();
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,7 +232,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 +244,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 +265,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 +290,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 +313,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 +330,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 +354,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);
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use crate::branch_picker::{self, BranchList};
|
||||
use crate::branch_picker::{self};
|
||||
use crate::git_panel_settings::StatusStyle;
|
||||
use crate::remote_output_toast::{RemoteAction, RemoteOutputToast};
|
||||
use crate::repository_selector::RepositorySelectorPopoverMenu;
|
||||
use crate::{
|
||||
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
|
||||
};
|
||||
use crate::{picker_prompt, project_diff, ProjectDiff};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::commit_tooltip::CommitTooltip;
|
||||
|
||||
use editor::{
|
||||
scroll::ScrollbarAutoHide, Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer,
|
||||
ShowScrollbar,
|
||||
@@ -40,8 +40,8 @@ use std::{collections::HashSet, sync::Arc, time::Duration, usize};
|
||||
use strum::{IntoEnumIterator, VariantNames};
|
||||
use time::OffsetDateTime;
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, Checkbox, ContextMenu, ElevationIndex, PopoverButton, PopoverMenu,
|
||||
Scrollbar, ScrollbarState, Tooltip,
|
||||
prelude::*, ButtonLike, Checkbox, ContextMenu, ElevationIndex, PopoverMenu, Scrollbar,
|
||||
ScrollbarState, Tooltip,
|
||||
};
|
||||
use util::{maybe, post_inc, ResultExt, TryFutureExt};
|
||||
|
||||
@@ -190,8 +190,7 @@ pub struct GitPanel {
|
||||
remote_operation_id: u32,
|
||||
pending_remote_operations: RemoteOperations,
|
||||
pub(crate) active_repository: Option<Entity<Repository>>,
|
||||
commit_editor: Entity<Editor>,
|
||||
pub(crate) suggested_commit_message: Option<String>,
|
||||
pub(crate) commit_editor: Entity<Editor>,
|
||||
conflicted_count: usize,
|
||||
conflicted_staged_count: usize,
|
||||
current_modifiers: Modifiers,
|
||||
@@ -206,7 +205,6 @@ pub struct GitPanel {
|
||||
pending_commit: Option<Task<()>>,
|
||||
pending_serialization: Task<Option<()>>,
|
||||
pub(crate) project: Entity<Project>,
|
||||
repository_selector: Entity<RepositorySelector>,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
scrollbar_state: ScrollbarState,
|
||||
selected_entry: Option<usize>,
|
||||
@@ -311,15 +309,11 @@ impl GitPanel {
|
||||
let scrollbar_state =
|
||||
ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity());
|
||||
|
||||
let repository_selector =
|
||||
cx.new(|cx| RepositorySelector::new(project.clone(), window, cx));
|
||||
|
||||
let mut git_panel = Self {
|
||||
pending_remote_operations: Default::default(),
|
||||
remote_operation_id: 0,
|
||||
active_repository,
|
||||
commit_editor,
|
||||
suggested_commit_message: None,
|
||||
conflicted_count: 0,
|
||||
conflicted_staged_count: 0,
|
||||
current_modifiers: window.modifiers(),
|
||||
@@ -334,7 +328,6 @@ impl GitPanel {
|
||||
pending_commit: None,
|
||||
pending_serialization: Task::ready(None),
|
||||
project,
|
||||
repository_selector,
|
||||
scroll_handle,
|
||||
scrollbar_state,
|
||||
selected_entry: None,
|
||||
@@ -696,12 +689,31 @@ impl GitPanel {
|
||||
fn open_diff(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
maybe!({
|
||||
let entry = self.entries.get(self.selected_entry?)?.status_entry()?;
|
||||
let workspace = self.workspace.upgrade()?;
|
||||
let git_repo = self.active_repository.as_ref()?;
|
||||
|
||||
if let Some(project_diff) = workspace.read(cx).active_item_as::<ProjectDiff>(cx) {
|
||||
if let Some(project_path) = project_diff.read(cx).active_path(cx) {
|
||||
if Some(&entry.repo_path)
|
||||
== git_repo
|
||||
.read(cx)
|
||||
.project_path_to_repo_path(&project_path)
|
||||
.as_ref()
|
||||
{
|
||||
project_diff.focus_handle(cx).focus(window);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
ProjectDiff::deploy_at(workspace, Some(entry.clone()), window, cx);
|
||||
})
|
||||
.ok()
|
||||
.ok();
|
||||
self.focus_handle.focus(window);
|
||||
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1154,6 +1166,17 @@ impl GitPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn custom_or_suggested_commit_message(&self, cx: &mut Context<Self>) -> Option<String> {
|
||||
let message = self.commit_editor.read(cx).text(cx);
|
||||
|
||||
if !message.trim().is_empty() {
|
||||
return Some(message.to_string());
|
||||
}
|
||||
|
||||
self.suggest_commit_message()
|
||||
.filter(|message| !message.trim().is_empty())
|
||||
}
|
||||
|
||||
pub(crate) fn commit_changes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(active_repository) = self.active_repository.clone() else {
|
||||
return;
|
||||
@@ -1175,11 +1198,13 @@ impl GitPanel {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut message = self.commit_editor.read(cx).text(cx);
|
||||
if message.trim().is_empty() {
|
||||
let commit_message = self.custom_or_suggested_commit_message(cx);
|
||||
|
||||
let Some(mut message) = commit_message else {
|
||||
self.commit_editor.read(cx).focus_handle(cx).focus(window);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if self.add_coauthors {
|
||||
self.fill_co_authors(&mut message, cx);
|
||||
}
|
||||
@@ -1310,7 +1335,7 @@ impl GitPanel {
|
||||
Some("Update")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}?;
|
||||
|
||||
let file_name = git_status_entry
|
||||
.repo_path
|
||||
@@ -1318,23 +1343,23 @@ impl GitPanel {
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy();
|
||||
|
||||
Some(format!("{} {}", action_text?, file_name))
|
||||
Some(format!("{} {}", action_text, file_name))
|
||||
}
|
||||
|
||||
fn update_editor_placeholder(&mut self, cx: &mut Context<Self>) {
|
||||
let suggested_commit_message = self.suggest_commit_message();
|
||||
let suggested_commit_message = suggested_commit_message
|
||||
let placeholder_text = suggested_commit_message
|
||||
.as_deref()
|
||||
.unwrap_or("Enter commit message");
|
||||
|
||||
self.commit_editor.update(cx, |editor, cx| {
|
||||
editor.set_placeholder_text(Arc::from(suggested_commit_message), cx)
|
||||
editor.set_placeholder_text(Arc::from(placeholder_text), cx)
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn fetch(&mut self, _: &git::Fetch, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
pub(crate) fn fetch(&mut self, _: &git::Fetch, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(repo) = self.active_repository.clone() else {
|
||||
return;
|
||||
};
|
||||
@@ -1361,7 +1386,7 @@ impl GitPanel {
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn pull(&mut self, _: &git::Pull, window: &mut Window, cx: &mut Context<Self>) {
|
||||
pub(crate) fn pull(&mut self, _: &git::Pull, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(repo) = self.active_repository.clone() else {
|
||||
return;
|
||||
};
|
||||
@@ -1369,7 +1394,6 @@ impl GitPanel {
|
||||
return;
|
||||
};
|
||||
let branch = branch.clone();
|
||||
let guard = self.start_remote_operation();
|
||||
let remote = self.get_current_remote(window, cx);
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let remote = match remote.await {
|
||||
@@ -1385,6 +1409,10 @@ impl GitPanel {
|
||||
}
|
||||
};
|
||||
|
||||
let guard = this
|
||||
.update(&mut cx, |this, _| this.start_remote_operation())
|
||||
.ok();
|
||||
|
||||
let pull = repo.update(&mut cx, |repo, _cx| {
|
||||
repo.pull(branch.name.clone(), remote.name.clone())
|
||||
})?;
|
||||
@@ -1405,7 +1433,7 @@ impl GitPanel {
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn push(&mut self, action: &git::Push, window: &mut Window, cx: &mut Context<Self>) {
|
||||
pub(crate) fn push(&mut self, action: &git::Push, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(repo) = self.active_repository.clone() else {
|
||||
return;
|
||||
};
|
||||
@@ -1413,7 +1441,6 @@ impl GitPanel {
|
||||
return;
|
||||
};
|
||||
let branch = branch.clone();
|
||||
let guard = self.start_remote_operation();
|
||||
let options = action.options;
|
||||
let remote = self.get_current_remote(window, cx);
|
||||
|
||||
@@ -1431,6 +1458,10 @@ impl GitPanel {
|
||||
}
|
||||
};
|
||||
|
||||
let guard = this
|
||||
.update(&mut cx, |this, _| this.start_remote_operation())
|
||||
.ok();
|
||||
|
||||
let push = repo.update(&mut cx, |repo, _cx| {
|
||||
repo.push(branch.name.clone(), remote.name.clone(), options)
|
||||
})?;
|
||||
@@ -1665,7 +1696,7 @@ impl GitPanel {
|
||||
git_panel.commit_editor = cx.new(|cx| {
|
||||
commit_message_editor(
|
||||
buffer,
|
||||
git_panel.suggested_commit_message.as_deref(),
|
||||
git_panel.suggest_commit_message().as_deref(),
|
||||
git_panel.project.clone(),
|
||||
true,
|
||||
window,
|
||||
@@ -1944,7 +1975,7 @@ impl GitPanel {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn configure_commit_button(&self, cx: &Context<Self>) -> (bool, &'static str) {
|
||||
pub fn configure_commit_button(&self, cx: &mut Context<Self>) -> (bool, &'static str) {
|
||||
if self.has_unstaged_conflicts() {
|
||||
(false, "You must resolve conflicts before committing")
|
||||
} else if !self.has_staged_changes() && !self.has_tracked_changes() {
|
||||
@@ -1954,19 +1985,20 @@ impl GitPanel {
|
||||
)
|
||||
} else if self.pending_commit.is_some() {
|
||||
(false, "Commit in progress")
|
||||
} else if self.commit_editor.read(cx).is_empty(cx) {
|
||||
} else if self.custom_or_suggested_commit_message(cx).is_none() {
|
||||
(false, "No commit message")
|
||||
} else if !self.has_write_access(cx) {
|
||||
(false, "You do not have write access to this project")
|
||||
} else {
|
||||
(
|
||||
true,
|
||||
if self.has_staged_changes() {
|
||||
"Commit"
|
||||
} else {
|
||||
"Commit Tracked"
|
||||
},
|
||||
)
|
||||
(true, self.commit_button_title())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn commit_button_title(&self) -> &'static str {
|
||||
if self.has_staged_changes() {
|
||||
"Commit"
|
||||
} else {
|
||||
"Commit Tracked"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1975,123 +2007,113 @@ impl GitPanel {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<impl IntoElement> {
|
||||
let active_repository = self.active_repository.clone()?;
|
||||
let (can_commit, tooltip) = self.configure_commit_button(cx);
|
||||
let project = self.project.clone().read(cx);
|
||||
let active_repository = self.active_repository.clone();
|
||||
let panel_editor_style = panel_editor_style(true, window, cx);
|
||||
|
||||
if let Some(active_repo) = active_repository {
|
||||
let (can_commit, tooltip) = self.configure_commit_button(cx);
|
||||
let enable_coauthors = self.render_co_authors(cx);
|
||||
|
||||
let enable_coauthors = self.render_co_authors(cx);
|
||||
let title = self.commit_button_title();
|
||||
let editor_focus_handle = self.commit_editor.focus_handle(cx);
|
||||
|
||||
let title = if self.has_staged_changes() {
|
||||
"Commit"
|
||||
} else {
|
||||
"Commit Tracked"
|
||||
};
|
||||
let editor_focus_handle = self.commit_editor.focus_handle(cx);
|
||||
let branch = active_repository.read(cx).current_branch().cloned();
|
||||
|
||||
let branch = active_repo.read(cx).current_branch()?.clone();
|
||||
let footer_size = px(32.);
|
||||
let gap = px(8.0);
|
||||
|
||||
let footer_size = px(32.);
|
||||
let gap = px(8.0);
|
||||
let max_height = window.line_height() * 5. + gap + footer_size;
|
||||
|
||||
let max_height = window.line_height() * 5. + gap + footer_size;
|
||||
let expand_button_size = px(16.);
|
||||
|
||||
let expand_button_size = px(16.);
|
||||
let git_panel = cx.entity().clone();
|
||||
let display_name = SharedString::from(Arc::from(
|
||||
active_repository
|
||||
.read(cx)
|
||||
.display_name(project, cx)
|
||||
.trim_end_matches("/"),
|
||||
));
|
||||
|
||||
let git_panel = cx.entity().clone();
|
||||
let display_name = SharedString::from(Arc::from(
|
||||
active_repo
|
||||
.read(cx)
|
||||
.display_name(project, cx)
|
||||
.trim_end_matches("/"),
|
||||
));
|
||||
let branches = branch_picker::popover(self.project.clone(), window, cx);
|
||||
let footer = v_flex()
|
||||
.child(PanelRepoFooter::new(
|
||||
"footer-button",
|
||||
display_name,
|
||||
Some(branch),
|
||||
Some(git_panel),
|
||||
Some(branches),
|
||||
))
|
||||
.child(
|
||||
panel_editor_container(window, cx)
|
||||
.id("commit-editor-container")
|
||||
.relative()
|
||||
.h(max_height)
|
||||
// .w_full()
|
||||
// .border_t_1()
|
||||
// .border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.cursor_text()
|
||||
.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
|
||||
window.focus(&this.commit_editor.focus_handle(cx));
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.id("commit-footer")
|
||||
.absolute()
|
||||
.bottom_0()
|
||||
.right_2()
|
||||
.h(footer_size)
|
||||
.flex_none()
|
||||
.children(enable_coauthors)
|
||||
.child(
|
||||
panel_filled_button(title)
|
||||
.tooltip(move |window, cx| {
|
||||
if can_commit {
|
||||
Tooltip::for_action_in(
|
||||
tooltip,
|
||||
&Commit,
|
||||
&editor_focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
Tooltip::simple(tooltip, cx)
|
||||
}
|
||||
let footer = v_flex()
|
||||
.child(PanelRepoFooter::new(
|
||||
"footer-button",
|
||||
display_name,
|
||||
branch,
|
||||
Some(git_panel),
|
||||
))
|
||||
.child(
|
||||
panel_editor_container(window, cx)
|
||||
.id("commit-editor-container")
|
||||
.relative()
|
||||
.h(max_height)
|
||||
// .w_full()
|
||||
// .border_t_1()
|
||||
// .border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.cursor_text()
|
||||
.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
|
||||
window.focus(&this.commit_editor.focus_handle(cx));
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.id("commit-footer")
|
||||
.absolute()
|
||||
.bottom_0()
|
||||
.right_2()
|
||||
.h(footer_size)
|
||||
.flex_none()
|
||||
.children(enable_coauthors)
|
||||
.child(
|
||||
panel_filled_button(title)
|
||||
.tooltip(move |window, cx| {
|
||||
if can_commit {
|
||||
Tooltip::for_action_in(
|
||||
tooltip,
|
||||
&Commit,
|
||||
&editor_focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
Tooltip::simple(tooltip, cx)
|
||||
}
|
||||
})
|
||||
.disabled(!can_commit || self.modal_open)
|
||||
.on_click({
|
||||
cx.listener(move |this, _: &ClickEvent, window, cx| {
|
||||
this.commit_changes(window, cx)
|
||||
})
|
||||
.disabled(!can_commit || self.modal_open)
|
||||
.on_click({
|
||||
cx.listener(move |this, _: &ClickEvent, window, cx| {
|
||||
this.commit_changes(window, cx)
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
// .when(!self.modal_open, |el| {
|
||||
.child(EditorElement::new(&self.commit_editor, panel_editor_style))
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_1()
|
||||
.right_2()
|
||||
.opacity(0.5)
|
||||
.hover(|this| this.opacity(1.0))
|
||||
.w(expand_button_size)
|
||||
.child(
|
||||
panel_icon_button("expand-commit-editor", IconName::Maximize)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.width(expand_button_size.into())
|
||||
.on_click(cx.listener({
|
||||
move |_, _, window, cx| {
|
||||
window.dispatch_action(
|
||||
git::ShowCommitEditor.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
)
|
||||
// .when(!self.modal_open, |el| {
|
||||
.child(EditorElement::new(&self.commit_editor, panel_editor_style))
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_1()
|
||||
.right_2()
|
||||
.opacity(0.5)
|
||||
.hover(|this| this.opacity(1.0))
|
||||
.w(expand_button_size)
|
||||
.child(
|
||||
panel_icon_button("expand-commit-editor", IconName::Maximize)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.width(expand_button_size.into())
|
||||
.on_click(cx.listener({
|
||||
move |_, _, window, cx| {
|
||||
window.dispatch_action(
|
||||
git::ShowCommitEditor.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Some(footer)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
Some(footer)
|
||||
}
|
||||
|
||||
fn render_previous_commit(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
|
||||
@@ -2118,7 +2140,7 @@ impl GitPanel {
|
||||
.child(
|
||||
Label::new(commit.subject.clone())
|
||||
.size(LabelSize::Small)
|
||||
.text_ellipsis(),
|
||||
.truncate(),
|
||||
)
|
||||
.id("commit-msg-hover")
|
||||
.hoverable_tooltip(move |window, cx| {
|
||||
@@ -2274,7 +2296,7 @@ impl GitPanel {
|
||||
) -> impl IntoElement {
|
||||
let entry_count = self.entries.len();
|
||||
|
||||
v_flex()
|
||||
h_flex()
|
||||
.size_full()
|
||||
.flex_grow()
|
||||
.overflow_hidden()
|
||||
@@ -2444,7 +2466,7 @@ impl GitPanel {
|
||||
ix: usize,
|
||||
entry: &GitStatusEntry,
|
||||
has_write_access: bool,
|
||||
_: &Window,
|
||||
window: &Window,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let display_name = entry
|
||||
@@ -2536,6 +2558,10 @@ impl GitPanel {
|
||||
.h(self.list_item_height())
|
||||
.w_full()
|
||||
.items_center()
|
||||
.border_1()
|
||||
.when(selected && self.focus_handle.is_focused(window), |el| {
|
||||
el.border_color(cx.theme().colors().border_focused)
|
||||
})
|
||||
.px(rems(0.75)) // ~12px
|
||||
.overflow_hidden()
|
||||
.flex_none()
|
||||
@@ -2551,6 +2577,7 @@ impl GitPanel {
|
||||
this.open_file(&Default::default(), window, cx)
|
||||
} else {
|
||||
this.open_diff(&Default::default(), window, cx);
|
||||
this.focus_handle.focus(window);
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -2687,9 +2714,6 @@ impl Render for GitPanel {
|
||||
.on_action(cx.listener(Self::unstage_all))
|
||||
.on_action(cx.listener(Self::restore_tracked_files))
|
||||
.on_action(cx.listener(Self::clean_all))
|
||||
.on_action(cx.listener(Self::fetch))
|
||||
.on_action(cx.listener(Self::pull))
|
||||
.on_action(cx.listener(Self::push))
|
||||
.when(has_write_access && has_co_authors, |git_panel| {
|
||||
git_panel.on_action(cx.listener(Self::toggle_fill_co_authors))
|
||||
})
|
||||
@@ -3041,7 +3065,6 @@ pub struct PanelRepoFooter {
|
||||
//
|
||||
// For now just take an option here, and we won't bind handlers to buttons in previews.
|
||||
git_panel: Option<Entity<GitPanel>>,
|
||||
branches: Option<Entity<BranchList>>,
|
||||
}
|
||||
|
||||
impl PanelRepoFooter {
|
||||
@@ -3050,14 +3073,12 @@ impl PanelRepoFooter {
|
||||
active_repository: SharedString,
|
||||
branch: Option<Branch>,
|
||||
git_panel: Option<Entity<GitPanel>>,
|
||||
branches: Option<Entity<BranchList>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
active_repository,
|
||||
branch,
|
||||
git_panel,
|
||||
branches,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3071,7 +3092,6 @@ impl PanelRepoFooter {
|
||||
active_repository,
|
||||
branch,
|
||||
git_panel: None,
|
||||
branches: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3293,51 +3313,50 @@ impl PanelRepoFooter {
|
||||
}
|
||||
|
||||
impl RenderOnce for PanelRepoFooter {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let active_repo = self.active_repository.clone();
|
||||
let overflow_menu_id: SharedString = format!("overflow-menu-{}", active_repo).into();
|
||||
let repo_selector_trigger = Button::new("repo-selector", active_repo)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.size(ButtonSize::None)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted);
|
||||
|
||||
let repo_selector = if let Some(panel) = self.git_panel.clone() {
|
||||
let repo_selector = panel.read(cx).repository_selector.clone();
|
||||
let repo_count = repo_selector.read(cx).repositories_len(cx);
|
||||
if repo_count > 1 {
|
||||
RepositorySelectorPopoverMenu::new(
|
||||
panel.read(cx).repository_selector.clone(),
|
||||
Button::new("repo-selector", active_repo)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.size(ButtonSize::None)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
Tooltip::text("Choose a repository"),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(active_repo)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.line_height_style(LineHeightStyle::UiLabel)
|
||||
.into_any_element()
|
||||
}
|
||||
} else {
|
||||
Button::new("repo-selector", active_repo.clone())
|
||||
.style(ButtonStyle::Transparent)
|
||||
.size(ButtonSize::None)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.into_any_element()
|
||||
};
|
||||
let project = self
|
||||
.git_panel
|
||||
.as_ref()
|
||||
.map(|panel| panel.read(cx).project.clone());
|
||||
|
||||
let single_repo = project
|
||||
.as_ref()
|
||||
.map(|project| project.read(cx).all_repositories(cx).len() == 1)
|
||||
.unwrap_or(true);
|
||||
|
||||
let repo_selector = PopoverMenu::new("repository-switcher")
|
||||
.menu({
|
||||
let project = project.clone();
|
||||
move |window, cx| {
|
||||
let project = project.clone()?;
|
||||
Some(cx.new(|cx| RepositorySelector::new(project, window, cx)))
|
||||
}
|
||||
})
|
||||
.trigger_with_tooltip(
|
||||
repo_selector_trigger.disabled(single_repo).truncate(true),
|
||||
Tooltip::text("Switch active repository"),
|
||||
)
|
||||
.attach(gpui::Corner::BottomLeft)
|
||||
.into_any_element();
|
||||
|
||||
let branch = self.branch.clone();
|
||||
let branch_name = branch
|
||||
.as_ref()
|
||||
.map_or("<no branch>".into(), |branch| branch.name.clone());
|
||||
|
||||
let branches = self.branches.clone();
|
||||
.map_or(" (no branch)".into(), |branch| branch.name.clone());
|
||||
|
||||
let branch_selector_button = Button::new("branch-selector", branch_name)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.size(ButtonSize::None)
|
||||
.label_size(LabelSize::Small)
|
||||
.truncate(true)
|
||||
.tooltip(Tooltip::for_action_title(
|
||||
"Switch Branch",
|
||||
&zed_actions::git::Branch,
|
||||
@@ -3346,18 +3365,17 @@ impl RenderOnce for PanelRepoFooter {
|
||||
window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
|
||||
});
|
||||
|
||||
let branch_selector = if let Some(branches) = branches {
|
||||
PopoverButton::new(
|
||||
branches,
|
||||
Corner::BottomLeft,
|
||||
let branch_selector = PopoverMenu::new("popover-button")
|
||||
.menu(move |window, cx| Some(branch_picker::popover(project.clone()?, window, cx)))
|
||||
.trigger_with_tooltip(
|
||||
branch_selector_button,
|
||||
Tooltip::for_action_title("Switch Branch", &zed_actions::git::Branch),
|
||||
)
|
||||
.render(window, cx)
|
||||
.into_any_element()
|
||||
} else {
|
||||
branch_selector_button.into_any_element()
|
||||
};
|
||||
.anchor(Corner::TopLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-2.0),
|
||||
});
|
||||
|
||||
let spinner = self
|
||||
.git_panel
|
||||
@@ -3372,36 +3390,31 @@ impl RenderOnce for PanelRepoFooter {
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.relative()
|
||||
.flex_1()
|
||||
.overflow_hidden()
|
||||
.items_center()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
// .when(repo_or_branch_has_uppercase, |this| {
|
||||
// this.relative().pt(px(2.))
|
||||
// })
|
||||
.child(
|
||||
Icon::new(IconName::GitBranchSmall)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
div().child(
|
||||
Icon::new(IconName::GitBranchSmall)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(repo_selector)
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.text_sm()
|
||||
.child("/"),
|
||||
)
|
||||
.child(branch_selector),
|
||||
),
|
||||
.child(repo_selector)
|
||||
.when_some(branch.clone(), |this, _| {
|
||||
this.child(
|
||||
div()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.text_sm()
|
||||
.child("/"),
|
||||
)
|
||||
})
|
||||
.child(branch_selector),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.flex_shrink_0()
|
||||
.children(spinner)
|
||||
.child(self.render_overflow_menu(overflow_menu_id))
|
||||
.when_some(branch, |this, branch| {
|
||||
@@ -3462,94 +3475,220 @@ impl ComponentPreview for PanelRepoFooter {
|
||||
}
|
||||
}
|
||||
|
||||
fn custom(branch_name: &str, upstream: Option<UpstreamTracking>) -> Branch {
|
||||
Branch {
|
||||
is_head: true,
|
||||
name: branch_name.to_string().into(),
|
||||
upstream: upstream.map(|tracking| Upstream {
|
||||
ref_name: format!("zed/{}", branch_name).into(),
|
||||
tracking,
|
||||
}),
|
||||
most_recent_commit: Some(CommitSummary {
|
||||
sha: "abc123".into(),
|
||||
subject: "Modify stuff".into(),
|
||||
commit_timestamp: 1710932954,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn active_repository(id: usize) -> SharedString {
|
||||
format!("repo-{}", id).into()
|
||||
}
|
||||
|
||||
let example_width = px(340.);
|
||||
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.w_full()
|
||||
.flex_none()
|
||||
.children(vec![example_group_with_title(
|
||||
"Action Button States",
|
||||
vec![
|
||||
single_example(
|
||||
"No Branch",
|
||||
div()
|
||||
.w(px(180.))
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
"no-branch",
|
||||
active_repository(1).clone(),
|
||||
None,
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
)
|
||||
.grow(),
|
||||
single_example(
|
||||
"Remote status unknown",
|
||||
div()
|
||||
.w(px(180.))
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
"unknown-upstream",
|
||||
active_repository(2).clone(),
|
||||
Some(branch(unknown_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
)
|
||||
.grow(),
|
||||
single_example(
|
||||
"No Remote Upstream",
|
||||
div()
|
||||
.w(px(180.))
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
"no-remote-upstream",
|
||||
active_repository(3).clone(),
|
||||
Some(branch(no_remote_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
)
|
||||
.grow(),
|
||||
single_example(
|
||||
"Not Ahead or Behind",
|
||||
div()
|
||||
.w(px(180.))
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
"not-ahead-or-behind",
|
||||
active_repository(4).clone(),
|
||||
Some(branch(not_ahead_or_behind_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
)
|
||||
.grow(),
|
||||
single_example(
|
||||
"Behind remote",
|
||||
div()
|
||||
.w(px(180.))
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
"behind-remote",
|
||||
active_repository(5).clone(),
|
||||
Some(branch(behind_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
)
|
||||
.grow(),
|
||||
single_example(
|
||||
"Ahead of remote",
|
||||
div()
|
||||
.w(px(180.))
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
"ahead-of-remote",
|
||||
active_repository(6).clone(),
|
||||
Some(branch(ahead_of_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
)
|
||||
.grow(),
|
||||
single_example(
|
||||
"Ahead and behind remote",
|
||||
div()
|
||||
.w(px(180.))
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
"ahead-and-behind",
|
||||
active_repository(7).clone(),
|
||||
Some(branch(ahead_and_behind_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
)
|
||||
.grow(),
|
||||
],
|
||||
)
|
||||
.grow()
|
||||
.vertical()])
|
||||
.children(vec![example_group_with_title(
|
||||
"Labels",
|
||||
vec![
|
||||
single_example(
|
||||
"Short Branch & Repo",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
"short-branch",
|
||||
SharedString::from("zed"),
|
||||
Some(custom("main", behind_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
)
|
||||
.grow(),
|
||||
single_example(
|
||||
"Long Branch",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
"long-branch",
|
||||
SharedString::from("zed"),
|
||||
Some(custom(
|
||||
"redesign-and-update-git-ui-list-entry-style",
|
||||
behind_upstream,
|
||||
)),
|
||||
))
|
||||
.into_any_element(),
|
||||
)
|
||||
.grow(),
|
||||
single_example(
|
||||
"Long Repo",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
"long-repo",
|
||||
SharedString::from("zed-industries-community-examples"),
|
||||
Some(custom("gpui", ahead_of_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
)
|
||||
.grow(),
|
||||
single_example(
|
||||
"Long Repo & Branch",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
"long-repo-and-branch",
|
||||
SharedString::from("zed-industries-community-examples"),
|
||||
Some(custom(
|
||||
"redesign-and-update-git-ui-list-entry-style",
|
||||
behind_upstream,
|
||||
)),
|
||||
))
|
||||
.into_any_element(),
|
||||
)
|
||||
.grow(),
|
||||
single_example(
|
||||
"Uppercase Repo",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
"uppercase-repo",
|
||||
SharedString::from("LICENSES"),
|
||||
Some(custom("main", ahead_of_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
)
|
||||
.grow(),
|
||||
single_example(
|
||||
"Uppercase Branch",
|
||||
div()
|
||||
.w(example_width)
|
||||
.overflow_hidden()
|
||||
.child(PanelRepoFooter::new_preview(
|
||||
"uppercase-branch",
|
||||
SharedString::from("zed"),
|
||||
Some(custom("update-README", behind_upstream)),
|
||||
))
|
||||
.into_any_element(),
|
||||
)
|
||||
.grow(),
|
||||
],
|
||||
)
|
||||
.grow()
|
||||
.vertical()])
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use git_panel_settings::GitPanelSettings;
|
||||
use gpui::App;
|
||||
use project_diff::ProjectDiff;
|
||||
use ui::{ActiveTheme, Color, Icon, IconName, IntoElement};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub mod branch_picker;
|
||||
mod commit_modal;
|
||||
@@ -19,6 +20,34 @@ pub fn init(cx: &mut App) {
|
||||
branch_picker::init(cx);
|
||||
cx.observe_new(ProjectDiff::register).detach();
|
||||
commit_modal::init(cx);
|
||||
|
||||
cx.observe_new(|workspace: &mut Workspace, _, _| {
|
||||
workspace.register_action(|workspace, fetch: &git::Fetch, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.fetch(fetch, window, cx);
|
||||
});
|
||||
});
|
||||
workspace.register_action(|workspace, push: &git::Push, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.push(push, window, cx);
|
||||
});
|
||||
});
|
||||
workspace.register_action(|workspace, pull: &git::Pull, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.pull(pull, window, cx);
|
||||
});
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
// TODO: Add updated status colors to theme
|
||||
|
||||
@@ -5,7 +5,7 @@ use collections::HashSet;
|
||||
use editor::{
|
||||
actions::{GoToHunk, GoToPreviousHunk},
|
||||
scroll::Autoscroll,
|
||||
Editor, EditorEvent, ToPoint,
|
||||
Editor, EditorEvent,
|
||||
};
|
||||
use feature_flags::FeatureFlagViewExt;
|
||||
use futures::StreamExt;
|
||||
@@ -192,6 +192,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 +257,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 +274,6 @@ impl ProjectDiff {
|
||||
unstage: has_staged_hunks,
|
||||
prev_next,
|
||||
selection,
|
||||
commit,
|
||||
stage_all,
|
||||
unstage_all,
|
||||
};
|
||||
@@ -271,41 +281,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 +395,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 +770,6 @@ struct ButtonStates {
|
||||
selection: bool,
|
||||
stage_all: bool,
|
||||
unstage_all: bool,
|
||||
commit: bool,
|
||||
}
|
||||
|
||||
impl Render for ProjectDiffToolbar {
|
||||
@@ -813,10 +808,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 +819,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 +835,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 +849,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 +862,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,7 +913,6 @@ impl Render for ProjectDiffToolbar {
|
||||
)
|
||||
.child(
|
||||
Button::new("commit", "Commit")
|
||||
.disabled(!button_states.commit)
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Commit",
|
||||
&ShowCommitEditor,
|
||||
@@ -964,12 +926,14 @@ impl Render for ProjectDiffToolbar {
|
||||
}
|
||||
}
|
||||
|
||||
#[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 +944,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);
|
||||
@@ -1152,4 +1121,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,6 +1,6 @@
|
||||
use gpui::{
|
||||
AnyElement, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
Subscription, Task, WeakEntity,
|
||||
AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription,
|
||||
Task, WeakEntity,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{
|
||||
@@ -8,7 +8,7 @@ use project::{
|
||||
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>>,
|
||||
@@ -47,10 +47,6 @@ impl RepositorySelector {
|
||||
}
|
||||
}
|
||||
|
||||
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>,
|
||||
@@ -82,54 +78,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>,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2045,6 +2045,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 +2063,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 +2109,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 +2726,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 +3492,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 +3504,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 +3804,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 +6057,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(
|
||||
|
||||
@@ -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,
|
||||
@@ -1362,8 +1371,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 +1426,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)) {
|
||||
|
||||
@@ -6,10 +6,7 @@ use client::ProjectId;
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use futures::StreamExt as _;
|
||||
use git::repository::{Branch, CommitDetails, PushOptions, Remote, RemoteCommandOutput, ResetMode};
|
||||
use git::{
|
||||
repository::{GitRepository, RepoPath},
|
||||
status::{GitSummary, TrackedSummary},
|
||||
};
|
||||
use git::repository::{GitRepository, RepoPath};
|
||||
use gpui::{
|
||||
App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
|
||||
WeakEntity,
|
||||
@@ -24,7 +21,7 @@ use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use text::BufferId;
|
||||
use util::{maybe, ResultExt};
|
||||
use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry};
|
||||
use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry, WorkDirectory};
|
||||
|
||||
pub struct GitStore {
|
||||
buffer_store: Entity<BufferStore>,
|
||||
@@ -691,6 +688,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 +1070,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,
|
||||
|
||||
@@ -4310,16 +4310,26 @@ impl Project {
|
||||
.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()?,
|
||||
))
|
||||
})
|
||||
|
||||
let mut found: Option<(Entity<Repository>, RepoPath)> = None;
|
||||
for repo_handle in self.git_store.read(cx).all_repositories() {
|
||||
let repo = repo_handle.read(cx);
|
||||
if repo.worktree_id != path.worktree_id {
|
||||
continue;
|
||||
}
|
||||
let Ok(relative_path) = repo.repository_entry.relativize(&path.path) else {
|
||||
continue;
|
||||
};
|
||||
if found
|
||||
.as_ref()
|
||||
.is_some_and(|(found, _)| repo.contains_sub_repo(found, cx))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
found = Some((repo_handle.clone(), relative_path))
|
||||
}
|
||||
|
||||
found
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -2241,6 +2241,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);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.177.0"
|
||||
version = "0.177.3"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
preview
|
||||
@@ -1020,7 +1020,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