Merge branch 'main' into update-assistant-styles

This commit is contained in:
Nate Butler 2023-07-10 10:22:18 -04:00
commit 4029481fd0
140 changed files with 4100 additions and 1106 deletions

View File

@ -16,8 +16,4 @@ jobs:
Restart your Zed or head to https://zed.dev/releases/stable/latest to grab it. Restart your Zed or head to https://zed.dev/releases/stable/latest to grab it.
```md
# Changelog
${{ github.event.release.body }} ${{ github.event.release.body }}
```

488
Cargo.lock generated
View File

@ -177,6 +177,28 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
[[package]]
name = "alsa"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8512c9117059663fb5606788fbca3619e2a91dac0e3fe516242eab1fa6be5e44"
dependencies = [
"alsa-sys",
"bitflags",
"libc",
"nix",
]
[[package]]
name = "alsa-sys"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
dependencies = [
"libc",
"pkg-config",
]
[[package]] [[package]]
name = "ambient-authority" name = "ambient-authority"
version = "0.0.1" version = "0.0.1"
@ -590,6 +612,19 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "audio"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"gpui",
"log",
"parking_lot 0.11.2",
"rodio",
"util",
]
[[package]] [[package]]
name = "auto_update" name = "auto_update"
version = "0.1.0" version = "0.1.0"
@ -756,6 +791,26 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "bindgen"
version = "0.64.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"lazy_static",
"lazycell",
"peeking_take_while",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn 1.0.109",
]
[[package]] [[package]]
name = "bindgen" name = "bindgen"
version = "0.65.1" version = "0.65.1"
@ -857,7 +912,7 @@ checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7"
dependencies = [ dependencies = [
"borsh-derive-internal", "borsh-derive-internal",
"borsh-schema-derive-internal", "borsh-schema-derive-internal",
"proc-macro-crate", "proc-macro-crate 0.1.5",
"proc-macro2", "proc-macro2",
"syn 1.0.109", "syn 1.0.109",
] ]
@ -986,6 +1041,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-broadcast", "async-broadcast",
"audio",
"client", "client",
"collections", "collections",
"fs", "fs",
@ -1082,6 +1138,12 @@ dependencies = [
"jobserver", "jobserver",
] ]
[[package]]
name = "cesu8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]] [[package]]
name = "cexpr" name = "cexpr"
version = "0.6.0" version = "0.6.0"
@ -1155,7 +1217,7 @@ dependencies = [
"bitflags", "bitflags",
"clap_derive 3.2.25", "clap_derive 3.2.25",
"clap_lex 0.2.4", "clap_lex 0.2.4",
"indexmap", "indexmap 1.9.3",
"once_cell", "once_cell",
"strsim", "strsim",
"termcolor", "termcolor",
@ -1226,6 +1288,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
[[package]]
name = "claxon"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688"
[[package]] [[package]]
name = "cli" name = "cli"
version = "0.1.0" version = "0.1.0"
@ -1333,10 +1401,11 @@ dependencies = [
[[package]] [[package]]
name = "collab" name = "collab"
version = "0.15.0" version = "0.16.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-tungstenite", "async-tungstenite",
"audio",
"axum", "axum",
"axum-extra", "axum-extra",
"base64 0.13.1", "base64 0.13.1",
@ -1415,6 +1484,7 @@ dependencies = [
"picker", "picker",
"postage", "postage",
"project", "project",
"recent_projects",
"serde", "serde",
"serde_derive", "serde_derive",
"settings", "settings",
@ -1444,6 +1514,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "combine"
version = "4.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4"
dependencies = [
"bytes 1.4.0",
"memchr",
]
[[package]] [[package]]
name = "command_palette" name = "command_palette"
version = "0.1.0" version = "0.1.0"
@ -1540,11 +1620,17 @@ name = "core-foundation"
version = "0.9.3" version = "0.9.3"
source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85" source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85"
dependencies = [ dependencies = [
"core-foundation-sys", "core-foundation-sys 0.8.3",
"libc", "libc",
"uuid 0.5.1", "uuid 0.5.1",
] ]
[[package]]
name = "core-foundation-sys"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b"
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.3" version = "0.8.3"
@ -1594,6 +1680,51 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "coreaudio-rs"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb17e2d1795b1996419648915df94bc7103c28f7b48062d7acf4652fc371b2ff"
dependencies = [
"bitflags",
"core-foundation-sys 0.6.2",
"coreaudio-sys",
]
[[package]]
name = "coreaudio-sys"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f034b2258e6c4ade2f73bf87b21047567fb913ee9550837c2316d139b0262b24"
dependencies = [
"bindgen 0.64.0",
]
[[package]]
name = "cpal"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d959d90e938c5493000514b446987c07aed46c668faaa7d34d6c7a67b1a578c"
dependencies = [
"alsa",
"core-foundation-sys 0.8.3",
"coreaudio-rs",
"dasp_sample",
"jni 0.19.0",
"js-sys",
"libc",
"mach2",
"ndk",
"ndk-context",
"oboe",
"once_cell",
"parking_lot 0.12.1",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows 0.46.0",
]
[[package]] [[package]]
name = "cpp_demangle" name = "cpp_demangle"
version = "0.3.5" version = "0.3.5"
@ -1924,6 +2055,12 @@ dependencies = [
"parking_lot_core 0.9.7", "parking_lot_core 0.9.7",
] ]
[[package]]
name = "dasp_sample"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
[[package]] [[package]]
name = "data-url" name = "data-url"
version = "0.1.1" version = "0.1.1"
@ -2233,6 +2370,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "equivalent"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
[[package]] [[package]]
name = "erased-serde" name = "erased-serde"
version = "0.3.25" version = "0.3.25"
@ -2549,6 +2692,7 @@ dependencies = [
"smol", "smol",
"sum_tree", "sum_tree",
"tempfile", "tempfile",
"time 0.3.21",
"util", "util",
] ]
@ -2793,7 +2937,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
dependencies = [ dependencies = [
"fallible-iterator", "fallible-iterator",
"indexmap", "indexmap 1.9.3",
"stable_deref_trait", "stable_deref_trait",
] ]
@ -2889,7 +3033,7 @@ dependencies = [
"anyhow", "anyhow",
"async-task", "async-task",
"backtrace", "backtrace",
"bindgen", "bindgen 0.65.1",
"block", "block",
"cc", "cc",
"cocoa", "cocoa",
@ -2961,7 +3105,7 @@ dependencies = [
"futures-sink", "futures-sink",
"futures-util", "futures-util",
"http", "http",
"indexmap", "indexmap 1.9.3",
"slab", "slab",
"tokio", "tokio",
"tokio-util 0.7.8", "tokio-util 0.7.8",
@ -2995,6 +3139,12 @@ dependencies = [
"ahash 0.8.3", "ahash 0.8.3",
] ]
[[package]]
name = "hashbrown"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
[[package]] [[package]]
name = "hashlink" name = "hashlink"
version = "0.8.1" version = "0.8.1"
@ -3105,6 +3255,12 @@ dependencies = [
"digest 0.10.6", "digest 0.10.6",
] ]
[[package]]
name = "hound"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d13cdbd5dbb29f9c88095bbdc2590c9cba0d0a1269b983fef6b2cdd7e9f4db1"
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.9" version = "0.2.9"
@ -3213,11 +3369,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c"
dependencies = [ dependencies = [
"android_system_properties", "android_system_properties",
"core-foundation-sys", "core-foundation-sys 0.8.3",
"iana-time-zone-haiku", "iana-time-zone-haiku",
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
"windows", "windows 0.48.0",
] ]
[[package]] [[package]]
@ -3287,6 +3443,16 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "indexmap"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
"equivalent",
"hashbrown 0.14.0",
]
[[package]] [[package]]
name = "indoc" name = "indoc"
version = "1.0.9" version = "1.0.9"
@ -3459,6 +3625,40 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "jni"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec"
dependencies = [
"cesu8",
"combine",
"jni-sys",
"log",
"thiserror",
"walkdir",
]
[[package]]
name = "jni"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c"
dependencies = [
"cesu8",
"combine",
"jni-sys",
"log",
"thiserror",
"walkdir",
]
[[package]]
name = "jni-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]] [[package]]
name = "jobserver" name = "jobserver"
version = "0.1.26" version = "0.1.26"
@ -3661,6 +3861,17 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
[[package]]
name = "lewton"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030"
dependencies = [
"byteorder",
"ogg",
"tinyvec",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.144" version = "0.2.144"
@ -3793,7 +4004,6 @@ dependencies = [
"gpui", "gpui",
"hmac 0.12.1", "hmac 0.12.1",
"jwt", "jwt",
"lazy_static",
"live_kit_server", "live_kit_server",
"log", "log",
"media", "media",
@ -3893,6 +4103,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "mach2"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "malloc_buf" name = "malloc_buf"
version = "0.0.6" version = "0.0.6"
@ -3949,7 +4168,7 @@ name = "media"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bindgen", "bindgen 0.65.1",
"block", "block",
"bytes 1.4.0", "bytes 1.4.0",
"core-foundation", "core-foundation",
@ -4207,6 +4426,35 @@ dependencies = [
"tempfile", "tempfile",
] ]
[[package]]
name = "ndk"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0"
dependencies = [
"bitflags",
"jni-sys",
"ndk-sys",
"num_enum",
"raw-window-handle",
"thiserror",
]
[[package]]
name = "ndk-context"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
[[package]]
name = "ndk-sys"
version = "0.4.1+23.1.7779620"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3"
dependencies = [
"jni-sys",
]
[[package]] [[package]]
name = "net2" name = "net2"
version = "0.2.38" version = "0.2.38"
@ -4315,6 +4563,17 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "num-derive"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.45" version = "0.1.45"
@ -4367,6 +4626,27 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "num_enum"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9"
dependencies = [
"num_enum_derive",
]
[[package]]
name = "num_enum_derive"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799"
dependencies = [
"proc-macro-crate 1.3.1",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "nvim-rs" name = "nvim-rs"
version = "0.5.0" version = "0.5.0"
@ -4409,7 +4689,7 @@ checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"hashbrown 0.11.2", "hashbrown 0.11.2",
"indexmap", "indexmap 1.9.3",
"memchr", "memchr",
] ]
@ -4422,6 +4702,38 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "oboe"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8868cc237ee02e2d9618539a23a8d228b9bb3fc2e7a5b11eed3831de77c395d0"
dependencies = [
"jni 0.20.0",
"ndk",
"ndk-context",
"num-derive",
"num-traits",
"oboe-sys",
]
[[package]]
name = "oboe-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f44155e7fb718d3cfddcf70690b2b51ac4412f347cd9e4fbe511abe9cd7b5f2"
dependencies = [
"cc",
]
[[package]]
name = "ogg"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e"
dependencies = [
"byteorder",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.17.1" version = "1.17.1"
@ -4711,7 +5023,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4"
dependencies = [ dependencies = [
"fixedbitset", "fixedbitset",
"indexmap", "indexmap 1.9.3",
] ]
[[package]] [[package]]
@ -4788,7 +5100,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590" checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590"
dependencies = [ dependencies = [
"base64 0.21.0", "base64 0.21.0",
"indexmap", "indexmap 1.9.3",
"line-wrap", "line-wrap",
"quick-xml", "quick-xml",
"serde", "serde",
@ -4921,6 +5233,16 @@ dependencies = [
"toml", "toml",
] ]
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
dependencies = [
"once_cell",
"toml_edit",
]
[[package]] [[package]]
name = "proc-macro-error" name = "proc-macro-error"
version = "1.0.4" version = "1.0.4"
@ -5033,6 +5355,7 @@ dependencies = [
"language", "language",
"menu", "menu",
"postage", "postage",
"pretty_assertions",
"project", "project",
"schemars", "schemars",
"serde", "serde",
@ -5332,6 +5655,12 @@ dependencies = [
"rand_core 0.5.1", "rand_core 0.5.1",
] ]
[[package]]
name = "raw-window-handle"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9"
[[package]] [[package]]
name = "rayon" name = "rayon"
version = "1.7.0" version = "1.7.0"
@ -5375,6 +5704,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"db", "db",
"editor", "editor",
"futures 0.3.28",
"fuzzy", "fuzzy",
"gpui", "gpui",
"language", "language",
@ -5615,6 +5945,19 @@ dependencies = [
"rmp", "rmp",
] ]
[[package]]
name = "rodio"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdf1d4dea18dff2e9eb6dca123724f8b60ef44ad74a9ad283cdfe025df7e73fa"
dependencies = [
"claxon",
"cpal",
"hound",
"lewton",
"symphonia",
]
[[package]] [[package]]
name = "rope" name = "rope"
version = "0.1.0" version = "0.1.0"
@ -6116,7 +6459,7 @@ checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"core-foundation", "core-foundation",
"core-foundation-sys", "core-foundation-sys 0.8.3",
"libc", "libc",
"security-framework-sys", "security-framework-sys",
] ]
@ -6127,7 +6470,7 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4"
dependencies = [ dependencies = [
"core-foundation-sys", "core-foundation-sys 0.8.3",
"libc", "libc",
] ]
@ -6201,7 +6544,7 @@ version = "1.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
dependencies = [ dependencies = [
"indexmap", "indexmap 1.9.3",
"itoa 1.0.6", "itoa 1.0.6",
"ryu", "ryu",
"serde", "serde",
@ -6213,7 +6556,7 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d7b9ce5b0a63c6269b9623ed828b39259545a6ec0d8a35d6135ad6af6232add" checksum = "7d7b9ce5b0a63c6269b9623ed828b39259545a6ec0d8a35d6135ad6af6232add"
dependencies = [ dependencies = [
"indexmap", "indexmap 1.9.3",
"itoa 0.4.8", "itoa 0.4.8",
"ryu", "ryu",
"serde", "serde",
@ -6248,7 +6591,7 @@ version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b"
dependencies = [ dependencies = [
"indexmap", "indexmap 1.9.3",
"ryu", "ryu",
"serde", "serde",
"yaml-rust", "yaml-rust",
@ -6622,7 +6965,7 @@ dependencies = [
"hex", "hex",
"hkdf", "hkdf",
"hmac 0.12.1", "hmac 0.12.1",
"indexmap", "indexmap 1.9.3",
"itoa 1.0.6", "itoa 1.0.6",
"libc", "libc",
"libsqlite3-sys", "libsqlite3-sys",
@ -6773,6 +7116,56 @@ dependencies = [
"siphasher", "siphasher",
] ]
[[package]]
name = "symphonia"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62e48dba70095f265fdb269b99619b95d04c89e619538138383e63310b14d941"
dependencies = [
"lazy_static",
"symphonia-bundle-mp3",
"symphonia-core",
"symphonia-metadata",
]
[[package]]
name = "symphonia-bundle-mp3"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f31d7fece546f1e6973011a9eceae948133bbd18fd3d52f6073b1e38ae6368a"
dependencies = [
"bitflags",
"lazy_static",
"log",
"symphonia-core",
"symphonia-metadata",
]
[[package]]
name = "symphonia-core"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c73eb88fee79705268cc7b742c7bc93a7b76e092ab751d0833866970754142"
dependencies = [
"arrayvec 0.7.2",
"bitflags",
"bytemuck",
"lazy_static",
"log",
]
[[package]]
name = "symphonia-metadata"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89c3e1937e31d0e068bbe829f66b2f2bfaa28d056365279e0ef897172c3320c0"
dependencies = [
"encoding_rs",
"lazy_static",
"log",
"symphonia-core",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.109" version = "1.0.109"
@ -6818,7 +7211,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a902e9050fca0a5d6877550b769abd2bd1ce8c04634b941dbe2809735e1a1e33" checksum = "a902e9050fca0a5d6877550b769abd2bd1ce8c04634b941dbe2809735e1a1e33"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"core-foundation-sys", "core-foundation-sys 0.8.3",
"libc", "libc",
"ntapi 0.4.1", "ntapi 0.4.1",
"once_cell", "once_cell",
@ -6987,7 +7380,7 @@ dependencies = [
"anyhow", "anyhow",
"fs", "fs",
"gpui", "gpui",
"indexmap", "indexmap 1.9.3",
"parking_lot 0.11.2", "parking_lot 0.11.2",
"schemars", "schemars",
"serde", "serde",
@ -7293,6 +7686,23 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "toml_datetime"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
[[package]]
name = "toml_edit"
version = "0.19.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7"
dependencies = [
"indexmap 2.0.0",
"toml_datetime",
"winnow",
]
[[package]] [[package]]
name = "tonic" name = "tonic"
version = "0.6.2" version = "0.6.2"
@ -7332,7 +7742,7 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-util", "futures-util",
"indexmap", "indexmap 1.9.3",
"pin-project", "pin-project",
"pin-project-lite 0.2.9", "pin-project-lite 0.2.9",
"rand 0.8.5", "rand 0.8.5",
@ -7987,7 +8397,6 @@ dependencies = [
"indoc", "indoc",
"itertools", "itertools",
"language", "language",
"lazy_static",
"log", "log",
"nvim-rs", "nvim-rs",
"parking_lot 0.11.2", "parking_lot 0.11.2",
@ -8189,7 +8598,7 @@ version = "0.85.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "570460c58b21e9150d2df0eaaedbb7816c34bcec009ae0dcc976e40ba81463e7" checksum = "570460c58b21e9150d2df0eaaedbb7816c34bcec009ae0dcc976e40ba81463e7"
dependencies = [ dependencies = [
"indexmap", "indexmap 1.9.3",
] ]
[[package]] [[package]]
@ -8203,7 +8612,7 @@ dependencies = [
"backtrace", "backtrace",
"bincode", "bincode",
"cfg-if 1.0.0", "cfg-if 1.0.0",
"indexmap", "indexmap 1.9.3",
"lazy_static", "lazy_static",
"libc", "libc",
"log", "log",
@ -8277,7 +8686,7 @@ dependencies = [
"anyhow", "anyhow",
"cranelift-entity", "cranelift-entity",
"gimli 0.26.2", "gimli 0.26.2",
"indexmap", "indexmap 1.9.3",
"log", "log",
"more-asserts", "more-asserts",
"object 0.28.4", "object 0.28.4",
@ -8347,7 +8756,7 @@ dependencies = [
"backtrace", "backtrace",
"cc", "cc",
"cfg-if 1.0.0", "cfg-if 1.0.0",
"indexmap", "indexmap 1.9.3",
"libc", "libc",
"log", "log",
"mach", "mach",
@ -8603,6 +9012,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdacb41e6a96a052c6cb63a144f24900236121c6f63f4f8219fef5977ecb0c25"
dependencies = [
"windows-targets 0.42.2",
]
[[package]] [[package]]
name = "windows" name = "windows"
version = "0.48.0" version = "0.48.0"
@ -8759,6 +9177,15 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winnow"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "winreg" name = "winreg"
version = "0.10.1" version = "0.10.1"
@ -8910,7 +9337,7 @@ dependencies = [
[[package]] [[package]]
name = "zed" name = "zed"
version = "0.94.0" version = "0.95.0"
dependencies = [ dependencies = [
"activity_indicator", "activity_indicator",
"ai", "ai",
@ -8919,6 +9346,7 @@ dependencies = [
"async-recursion 0.3.2", "async-recursion 0.3.2",
"async-tar", "async-tar",
"async-trait", "async-trait",
"audio",
"auto_update", "auto_update",
"backtrace", "backtrace",
"breadcrumbs", "breadcrumbs",
@ -8948,7 +9376,7 @@ dependencies = [
"gpui", "gpui",
"ignore", "ignore",
"image", "image",
"indexmap", "indexmap 1.9.3",
"install_cli", "install_cli",
"isahc", "isahc",
"journal", "journal",

View File

@ -2,6 +2,7 @@
members = [ members = [
"crates/activity_indicator", "crates/activity_indicator",
"crates/ai", "crates/ai",
"crates/audio",
"crates/auto_update", "crates/auto_update",
"crates/breadcrumbs", "crates/breadcrumbs",
"crates/call", "crates/call",
@ -101,6 +102,7 @@ time = { version = "0.3", features = ["serde", "serde-well-known"] }
toml = { version = "0.5" } toml = { version = "0.5" }
tree-sitter = "0.20" tree-sitter = "0.20"
unindent = { version = "0.1.7" } unindent = { version = "0.1.7" }
pretty_assertions = "1.3.0"
[patch.crates-io] [patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "49226023693107fba9a1191136a4f47f38cdca73" } tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "49226023693107fba9a1191136a4f47f38cdca73" }

View File

@ -24,9 +24,7 @@
], ],
"ctrl-shift-down": "editor::AddSelectionBelow", "ctrl-shift-down": "editor::AddSelectionBelow",
"ctrl-shift-up": "editor::AddSelectionAbove", "ctrl-shift-up": "editor::AddSelectionAbove",
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine", "cmd-shift-backspace": "editor::DeleteToBeginningOfLine"
"cmd-shift-enter": "editor::NewlineAbove",
"cmd-enter": "editor::NewlineBelow"
} }
}, },
{ {

View File

@ -24,9 +24,7 @@
"ctrl-.": "editor::GoToHunk", "ctrl-.": "editor::GoToHunk",
"ctrl-,": "editor::GoToPrevHunk", "ctrl-,": "editor::GoToPrevHunk",
"ctrl-backspace": "editor::DeleteToPreviousWordStart", "ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd", "ctrl-delete": "editor::DeleteToNextWordEnd"
"cmd-shift-enter": "editor::NewlineAbove",
"cmd-enter": "editor::NewlineBelow"
} }
}, },
{ {

View File

@ -12,8 +12,6 @@
"ctrl-shift-d": "editor::DuplicateLine", "ctrl-shift-d": "editor::DuplicateLine",
"cmd-b": "editor::GoToDefinition", "cmd-b": "editor::GoToDefinition",
"cmd-j": "editor::ScrollCursorCenter", "cmd-j": "editor::ScrollCursorCenter",
"cmd-alt-enter": "editor::NewlineAbove",
"cmd-enter": "editor::NewlineBelow",
"cmd-shift-l": "editor::SelectLine", "cmd-shift-l": "editor::SelectLine",
"cmd-shift-t": "outline::Toggle", "cmd-shift-t": "outline::Toggle",
"alt-backspace": "editor::DeleteToPreviousWordStart", "alt-backspace": "editor::DeleteToPreviousWordStart",
@ -56,7 +54,9 @@
}, },
{ {
"context": "Editor && mode == full", "context": "Editor && mode == full",
"bindings": {} "bindings": {
"cmd-alt-enter": "editor::NewlineAbove"
}
}, },
{ {
"context": "BufferSearchBar", "context": "BufferSearchBar",

View File

@ -35,8 +35,11 @@
"l": "vim::Right", "l": "vim::Right",
"right": "vim::Right", "right": "vim::Right",
"$": "vim::EndOfLine", "$": "vim::EndOfLine",
"^": "vim::FirstNonWhitespace",
"shift-g": "vim::EndOfDocument", "shift-g": "vim::EndOfDocument",
"w": "vim::NextWordStart", "w": "vim::NextWordStart",
"{": "vim::StartOfParagraph",
"}": "vim::EndOfParagraph",
"shift-w": [ "shift-w": [
"vim::NextWordStart", "vim::NextWordStart",
{ {
@ -92,7 +95,10 @@
], ],
"ctrl-o": "pane::GoBack", "ctrl-o": "pane::GoBack",
"ctrl-]": "editor::GoToDefinition", "ctrl-]": "editor::GoToDefinition",
"escape": "editor::Cancel", "escape": [
"vim::SwitchMode",
"Normal"
],
"0": "vim::StartOfLine", // When no number operator present, use start of line motion "0": "vim::StartOfLine", // When no number operator present, use start of line motion
"1": [ "1": [
"vim::Number", "vim::Number",
@ -165,7 +171,6 @@
"shift-a": "vim::InsertEndOfLine", "shift-a": "vim::InsertEndOfLine",
"x": "vim::DeleteRight", "x": "vim::DeleteRight",
"shift-x": "vim::DeleteLeft", "shift-x": "vim::DeleteLeft",
"^": "vim::FirstNonWhitespace",
"o": "vim::InsertLineBelow", "o": "vim::InsertLineBelow",
"shift-o": "vim::InsertLineAbove", "shift-o": "vim::InsertLineAbove",
"~": "vim::ChangeCase", "~": "vim::ChangeCase",
@ -305,6 +310,10 @@
"vim::PushOperator", "vim::PushOperator",
"Replace" "Replace"
], ],
"ctrl-c": [
"vim::SwitchMode",
"Normal"
],
"> >": "editor::Indent", "> >": "editor::Indent",
"< <": "editor::Outdent" "< <": "editor::Outdent"
} }
@ -321,7 +330,10 @@
"bindings": { "bindings": {
"tab": "vim::Tab", "tab": "vim::Tab",
"enter": "vim::Enter", "enter": "vim::Enter",
"escape": "editor::Cancel" "escape": [
"vim::SwitchMode",
"Normal"
]
} }
} }
] ]

View File

@ -71,7 +71,9 @@
// "never" // "never"
"show": "auto", "show": "auto",
// Whether to show git diff indicators in the scrollbar. // Whether to show git diff indicators in the scrollbar.
"git_diff": true "git_diff": true,
// Whether to show selections in the scrollbar.
"selections": true
}, },
// Inlay hint related settings // Inlay hint related settings
"inlay_hints": { "inlay_hints": {

Binary file not shown.

Binary file not shown.

BIN
assets/sounds/mute.wav Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/sounds/unmute.wav Normal file

Binary file not shown.

View File

@ -12,6 +12,7 @@ use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
cmp::Reverse, cmp::Reverse,
ffi::OsStr,
fmt::{self, Display}, fmt::{self, Display},
path::PathBuf, path::PathBuf,
sync::Arc, sync::Arc,
@ -80,6 +81,9 @@ impl SavedConversationMetadata {
let mut conversations = Vec::<SavedConversationMetadata>::new(); let mut conversations = Vec::<SavedConversationMetadata>::new();
while let Some(path) = paths.next().await { while let Some(path) = paths.next().await {
let path = path?; let path = path?;
if path.extension() != Some(OsStr::new("json")) {
continue;
}
let pattern = r" - \d+.zed.json$"; let pattern = r" - \d+.zed.json$";
let re = Regex::new(pattern).unwrap(); let re = Regex::new(pattern).unwrap();

View File

@ -147,8 +147,9 @@ impl AssistantPanel {
.await .await
.log_err() .log_err()
.unwrap_or_default(); .unwrap_or_default();
this.update(&mut cx, |this, _| { this.update(&mut cx, |this, cx| {
this.saved_conversations = saved_conversations this.saved_conversations = saved_conversations;
cx.notify();
}) })
.ok(); .ok();
} }
@ -1911,7 +1912,7 @@ impl ConversationEditor {
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else { let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
return; return;
}; };
let Some(editor) = workspace.active_item(cx).and_then(|item| item.downcast::<Editor>()) else { let Some(editor) = workspace.active_item(cx).and_then(|item| item.act_as::<Editor>(cx)) else {
return; return;
}; };

23
crates/audio/Cargo.toml Normal file
View File

@ -0,0 +1,23 @@
[package]
name = "audio"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/audio.rs"
doctest = false
[dependencies]
gpui = { path = "../gpui" }
collections = { path = "../collections" }
util = { path = "../util" }
rodio = "0.17.1"
log.workspace = true
anyhow.workspace = true
parking_lot.workspace = true
[dev-dependencies]

View File

@ -0,0 +1,44 @@
use std::{io::Cursor, sync::Arc};
use anyhow::Result;
use collections::HashMap;
use gpui::{AppContext, AssetSource};
use rodio::{
source::{Buffered, SamplesConverter},
Decoder, Source,
};
type Sound = Buffered<SamplesConverter<Decoder<Cursor<Vec<u8>>>, f32>>;
pub struct SoundRegistry {
cache: Arc<parking_lot::Mutex<HashMap<String, Sound>>>,
assets: Box<dyn AssetSource>,
}
impl SoundRegistry {
pub fn new(source: impl AssetSource) -> Arc<Self> {
Arc::new(Self {
cache: Default::default(),
assets: Box::new(source),
})
}
pub fn global(cx: &AppContext) -> Arc<Self> {
cx.global::<Arc<Self>>().clone()
}
pub fn get(&self, name: &str) -> Result<impl Source<Item = f32>> {
if let Some(wav) = self.cache.lock().get(name) {
return Ok(wav.clone());
}
let path = format!("sounds/{}.wav", name);
let bytes = self.assets.load(&path)?.into_owned();
let cursor = Cursor::new(bytes);
let source = Decoder::new(cursor)?.convert_samples::<f32>().buffered();
self.cache.lock().insert(name.to_string(), source.clone());
Ok(source)
}
}

67
crates/audio/src/audio.rs Normal file
View File

@ -0,0 +1,67 @@
use assets::SoundRegistry;
use gpui::{AppContext, AssetSource};
use rodio::{OutputStream, OutputStreamHandle};
use util::ResultExt;
mod assets;
pub fn init(source: impl AssetSource, cx: &mut AppContext) {
cx.set_global(SoundRegistry::new(source));
cx.set_global(Audio::new());
}
pub enum Sound {
Joined,
Leave,
Mute,
Unmute,
StartScreenshare,
StopScreenshare,
}
impl Sound {
fn file(&self) -> &'static str {
match self {
Self::Joined => "joined_call",
Self::Leave => "leave_call",
Self::Mute => "mute",
Self::Unmute => "unmute",
Self::StartScreenshare => "start_screenshare",
Self::StopScreenshare => "stop_screenshare",
}
}
}
pub struct Audio {
_output_stream: Option<OutputStream>,
output_handle: Option<OutputStreamHandle>,
}
impl Audio {
pub fn new() -> Self {
let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip();
Self {
_output_stream,
output_handle,
}
}
pub fn play_sound(sound: Sound, cx: &AppContext) {
if !cx.has_global::<Self>() {
return;
}
let this = cx.global::<Self>();
let Some(output_handle) = this.output_handle.as_ref() else {
return;
};
let Some(source) = SoundRegistry::global(cx).get(sound.file()).log_err() else {
return;
};
output_handle.play_raw(source).log_err();
}
}

View File

@ -19,6 +19,7 @@ test-support = [
] ]
[dependencies] [dependencies]
audio = { path = "../audio" }
client = { path = "../client" } client = { path = "../client" }
collections = { path = "../collections" } collections = { path = "../collections" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }

View File

@ -3,6 +3,7 @@ use crate::{
IncomingCall, IncomingCall,
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use audio::{Audio, Sound};
use client::{ use client::{
proto::{self, PeerId}, proto::{self, PeerId},
Client, TypedEnvelope, User, UserStore, Client, TypedEnvelope, User, UserStore,
@ -151,6 +152,7 @@ impl Room {
let connect = room.connect(&connection_info.server_url, &connection_info.token); let connect = room.connect(&connection_info.server_url, &connection_info.token);
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
connect.await?; connect.await?;
this.update(&mut cx, |this, cx| this.share_microphone(cx)) this.update(&mut cx, |this, cx| this.share_microphone(cx))
.await?; .await?;
@ -176,6 +178,8 @@ impl Room {
let maintain_connection = let maintain_connection =
cx.spawn_weak(|this, cx| Self::maintain_connection(this, client.clone(), cx).log_err()); cx.spawn_weak(|this, cx| Self::maintain_connection(this, client.clone(), cx).log_err());
Audio::play_sound(Sound::Joined, cx);
Self { Self {
id, id,
live_kit: live_kit_room, live_kit: live_kit_room,
@ -265,6 +269,7 @@ impl Room {
room.apply_room_update(room_proto, cx)?; room.apply_room_update(room_proto, cx)?;
anyhow::Ok(()) anyhow::Ok(())
})?; })?;
Ok(room) Ok(room)
}) })
} }
@ -306,6 +311,8 @@ impl Room {
} }
} }
Audio::play_sound(Sound::Leave, cx);
self.status = RoomStatus::Offline; self.status = RoomStatus::Offline;
self.remote_participants.clear(); self.remote_participants.clear();
self.pending_participants.clear(); self.pending_participants.clear();
@ -656,6 +663,8 @@ impl Room {
}, },
); );
Audio::play_sound(Sound::Joined, cx);
if let Some(live_kit) = this.live_kit.as_ref() { if let Some(live_kit) = this.live_kit.as_ref() {
let video_tracks = let video_tracks =
live_kit.room.remote_video_tracks(&user.id.to_string()); live_kit.room.remote_video_tracks(&user.id.to_string());
@ -922,6 +931,7 @@ impl Room {
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
let project = let project =
Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?; Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.joined_projects.retain(|project| { this.joined_projects.retain(|project| {
if let Some(project) = project.upgrade(cx) { if let Some(project) = project.upgrade(cx) {
@ -1212,6 +1222,9 @@ impl Room {
}; };
cx.notify(); cx.notify();
} }
Audio::play_sound(Sound::StartScreenshare, cx);
Ok(()) Ok(())
} }
Err(error) => { Err(error) => {
@ -1227,38 +1240,20 @@ impl Room {
}) })
}) })
} }
fn set_mute(
live_kit: &mut LiveKitRoom,
should_mute: bool,
cx: &mut ModelContext<Self>,
) -> Result<Task<Result<()>>> {
if !should_mute {
// clear user muting state.
live_kit.muted_by_user = false;
}
match &mut live_kit.microphone_track {
LocalTrack::None => Err(anyhow!("microphone was not shared")),
LocalTrack::Pending { muted, .. } => {
*muted = should_mute;
cx.notify();
Ok(Task::Ready(Some(Ok(()))))
}
LocalTrack::Published {
track_publication,
muted,
} => {
*muted = should_mute;
cx.notify();
Ok(cx.background().spawn(track_publication.set_mute(*muted)))
}
}
}
pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> { pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
let should_mute = !self.is_muted(); let should_mute = !self.is_muted();
if let Some(live_kit) = self.live_kit.as_mut() { if let Some(live_kit) = self.live_kit.as_mut() {
let ret = Self::set_mute(live_kit, should_mute, cx); let (ret_task, old_muted) = live_kit.set_mute(should_mute, cx)?;
live_kit.muted_by_user = should_mute; live_kit.muted_by_user = should_mute;
ret
if old_muted == true && live_kit.deafened == true {
if let Some(task) = self.toggle_deafen(cx).ok() {
task.detach();
}
}
Ok(ret_task)
} else { } else {
Err(anyhow!("LiveKit not started")) Err(anyhow!("LiveKit not started"))
} }
@ -1274,7 +1269,7 @@ impl Room {
// When deafening, mute user's mic as well. // When deafening, mute user's mic as well.
// When undeafening, unmute user's mic unless it was manually muted prior to deafening. // When undeafening, unmute user's mic unless it was manually muted prior to deafening.
if live_kit.deafened || !live_kit.muted_by_user { if live_kit.deafened || !live_kit.muted_by_user {
mute_task = Some(Self::set_mute(live_kit, live_kit.deafened, cx)?); mute_task = Some(live_kit.set_mute(live_kit.deafened, cx)?.0);
}; };
for participant in self.remote_participants.values() { for participant in self.remote_participants.values() {
for track in live_kit for track in live_kit
@ -1319,6 +1314,8 @@ impl Room {
} => { } => {
live_kit.room.unpublish_track(track_publication); live_kit.room.unpublish_track(track_publication);
cx.notify(); cx.notify();
Audio::play_sound(Sound::StopScreenshare, cx);
Ok(()) Ok(())
} }
} }
@ -1347,6 +1344,51 @@ struct LiveKitRoom {
_maintain_tracks: [Task<()>; 2], _maintain_tracks: [Task<()>; 2],
} }
impl LiveKitRoom {
fn set_mute(
self: &mut LiveKitRoom,
should_mute: bool,
cx: &mut ModelContext<Room>,
) -> Result<(Task<Result<()>>, bool)> {
if !should_mute {
// clear user muting state.
self.muted_by_user = false;
}
let (result, old_muted) = match &mut self.microphone_track {
LocalTrack::None => Err(anyhow!("microphone was not shared")),
LocalTrack::Pending { muted, .. } => {
let old_muted = *muted;
*muted = should_mute;
cx.notify();
Ok((Task::Ready(Some(Ok(()))), old_muted))
}
LocalTrack::Published {
track_publication,
muted,
} => {
let old_muted = *muted;
*muted = should_mute;
cx.notify();
Ok((
cx.background().spawn(track_publication.set_mute(*muted)),
old_muted,
))
}
}?;
if old_muted != should_mute {
if should_mute {
Audio::play_sound(Sound::Mute, cx);
} else {
Audio::play_sound(Sound::Unmute, cx);
}
}
Ok((result, old_muted))
}
}
enum LocalTrack { enum LocalTrack {
None, None,
Pending { Pending {

View File

@ -201,6 +201,7 @@ impl Bundle {
self.zed_version_string() self.zed_version_string()
); );
} }
Self::LocalPath { executable, .. } => { Self::LocalPath { executable, .. } => {
let executable_parent = executable let executable_parent = executable
.parent() .parent()

View File

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab" default-run = "collab"
edition = "2021" edition = "2021"
name = "collab" name = "collab"
version = "0.15.0" version = "0.16.0"
publish = false publish = false
[[bin]] [[bin]]
@ -57,6 +57,7 @@ tracing-log = "0.1.3"
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] } tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
[dev-dependencies] [dev-dependencies]
audio = { path = "../audio" }
collections = { path = "../collections", features = ["test-support"] } collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
call = { path = "../call", features = ["test-support"] } call = { path = "../call", features = ["test-support"] }
@ -67,7 +68,7 @@ fs = { path = "../fs", features = ["test-support"] }
git = { path = "../git", features = ["test-support"] } git = { path = "../git", features = ["test-support"] }
live_kit_client = { path = "../live_kit_client", features = ["test-support"] } live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] }
pretty_assertions = "1.3.0" pretty_assertions.workspace = true
project = { path = "../project", features = ["test-support"] } project = { path = "../project", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] }

View File

@ -3517,7 +3517,6 @@ pub use test::*;
mod test { mod test {
use super::*; use super::*;
use gpui::executor::Background; use gpui::executor::Background;
use lazy_static::lazy_static;
use parking_lot::Mutex; use parking_lot::Mutex;
use sea_orm::ConnectionTrait; use sea_orm::ConnectionTrait;
use sqlx::migrate::MigrateDatabase; use sqlx::migrate::MigrateDatabase;
@ -3566,9 +3565,7 @@ mod test {
} }
pub fn postgres(background: Arc<Background>) -> Self { pub fn postgres(background: Arc<Background>) -> Self {
lazy_static! { static LOCK: Mutex<()> = Mutex::new(());
static ref LOCK: Mutex<()> = Mutex::new(());
}
let _guard = LOCK.lock(); let _guard = LOCK.lock();
let mut rng = StdRng::from_entropy(); let mut rng = StdRng::from_entropy();

View File

@ -203,6 +203,7 @@ impl TestServer {
language::init(cx); language::init(cx);
editor::init_settings(cx); editor::init_settings(cx);
workspace::init(app_state.clone(), cx); workspace::init(app_state.clone(), cx);
audio::init((), cx);
call::init(client.clone(), user_store.clone(), cx); call::init(client.clone(), user_store.clone(), cx);
}); });

View File

@ -18,7 +18,7 @@ use gpui::{
}; };
use indoc::indoc; use indoc::indoc;
use language::{ use language::{
language_settings::{AllLanguageSettings, Formatter, InlayHintKind, InlayHintSettings}, language_settings::{AllLanguageSettings, Formatter, InlayHintSettings},
tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
LanguageConfig, OffsetRangeExt, Point, Rope, LanguageConfig, OffsetRangeExt, Point, Rope,
}; };
@ -7843,7 +7843,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
}); });
}); });
}); });
let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
let mut language = Language::new( let mut language = Language::new(
LanguageConfig { LanguageConfig {
@ -7955,10 +7954,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
"Host should get its first hints when opens an editor" "Host should get its first hints when opens an editor"
); );
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
"Cache should use editor settings to get the allowed hint kinds"
);
assert_eq!( assert_eq!(
inlay_cache.version, edits_made, inlay_cache.version, edits_made,
"Host editor update the cache version after every cache/view change", "Host editor update the cache version after every cache/view change",
@ -7982,10 +7977,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
"Client should get its first hints when opens an editor" "Client should get its first hints when opens an editor"
); );
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
"Cache should use editor settings to get the allowed hint kinds"
);
assert_eq!( assert_eq!(
inlay_cache.version, edits_made, inlay_cache.version, edits_made,
"Guest editor update the cache version after every cache/view change" "Guest editor update the cache version after every cache/view change"
@ -8007,10 +7998,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
"Host should get hints from the 1st edit and 1st LSP query" "Host should get hints from the 1st edit and 1st LSP query"
); );
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
"Inlay kinds settings never change during the test"
);
assert_eq!(inlay_cache.version, edits_made); assert_eq!(inlay_cache.version, edits_made);
}); });
editor_b.update(cx_b, |editor, _| { editor_b.update(cx_b, |editor, _| {
@ -8025,10 +8012,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
"Guest should get hints the 1st edit and 2nd LSP query" "Guest should get hints the 1st edit and 2nd LSP query"
); );
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
"Inlay kinds settings never change during the test"
);
assert_eq!(inlay_cache.version, edits_made); assert_eq!(inlay_cache.version, edits_made);
}); });
@ -8054,10 +8037,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
4th query was made by guest (but not applied) due to cache invalidation logic" 4th query was made by guest (but not applied) due to cache invalidation logic"
); );
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
"Inlay kinds settings never change during the test"
);
assert_eq!(inlay_cache.version, edits_made); assert_eq!(inlay_cache.version, edits_made);
}); });
editor_b.update(cx_b, |editor, _| { editor_b.update(cx_b, |editor, _| {
@ -8074,10 +8053,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
"Guest should get hints from 3rd edit, 6th LSP query" "Guest should get hints from 3rd edit, 6th LSP query"
); );
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
"Inlay kinds settings never change during the test"
);
assert_eq!(inlay_cache.version, edits_made); assert_eq!(inlay_cache.version, edits_made);
}); });
@ -8103,10 +8078,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
"Host should react to /refresh LSP request and get new hints from 7th LSP query" "Host should react to /refresh LSP request and get new hints from 7th LSP query"
); );
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
"Inlay kinds settings never change during the test"
);
assert_eq!( assert_eq!(
inlay_cache.version, edits_made, inlay_cache.version, edits_made,
"Host should accepted all edits and bump its cache version every time" "Host should accepted all edits and bump its cache version every time"
@ -8128,10 +8099,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
"Guest should get a /refresh LSP request propagated by host and get new hints from 8th LSP query" "Guest should get a /refresh LSP request propagated by host and get new hints from 8th LSP query"
); );
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
"Inlay kinds settings never change during the test"
);
assert_eq!( assert_eq!(
inlay_cache.version, inlay_cache.version,
edits_made, edits_made,
@ -8164,9 +8131,9 @@ async fn test_inlay_hint_refresh_is_forwarded(
store.update_user_settings::<AllLanguageSettings>(cx, |settings| { store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings { settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: false, enabled: false,
show_type_hints: true, show_type_hints: false,
show_parameter_hints: false, show_parameter_hints: false,
show_other_hints: true, show_other_hints: false,
}) })
}); });
}); });
@ -8177,13 +8144,12 @@ async fn test_inlay_hint_refresh_is_forwarded(
settings.defaults.inlay_hints = Some(InlayHintSettings { settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: true, enabled: true,
show_type_hints: true, show_type_hints: true,
show_parameter_hints: false, show_parameter_hints: true,
show_other_hints: true, show_other_hints: true,
}) })
}); });
}); });
}); });
let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
let mut language = Language::new( let mut language = Language::new(
LanguageConfig { LanguageConfig {
@ -8299,10 +8265,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
"Host should get no hints due to them turned off" "Host should get no hints due to them turned off"
); );
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
"Host should have allowed hint kinds set despite hints are off"
);
assert_eq!( assert_eq!(
inlay_cache.version, 0, inlay_cache.version, 0,
"Host should not increment its cache version due to no changes", "Host should not increment its cache version due to no changes",
@ -8318,10 +8280,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
"Client should get its first hints when opens an editor" "Client should get its first hints when opens an editor"
); );
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
"Cache should use editor settings to get the allowed hint kinds"
);
assert_eq!( assert_eq!(
inlay_cache.version, edits_made, inlay_cache.version, edits_made,
"Guest editor update the cache version after every cache/view change" "Guest editor update the cache version after every cache/view change"
@ -8339,7 +8297,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
"Host should get nop hints due to them turned off, even after the /refresh" "Host should get nop hints due to them turned off, even after the /refresh"
); );
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
assert_eq!( assert_eq!(
inlay_cache.version, 0, inlay_cache.version, 0,
"Host should not increment its cache version due to no changes", "Host should not increment its cache version due to no changes",
@ -8355,10 +8312,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
"Guest should get a /refresh LSP request propagated by host despite host hints are off" "Guest should get a /refresh LSP request propagated by host despite host hints are off"
); );
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
"Inlay kinds settings never change during the test"
);
assert_eq!( assert_eq!(
inlay_cache.version, edits_made, inlay_cache.version, edits_made,
"Guest should accepted all edits and bump its cache version every time" "Guest should accepted all edits and bump its cache version every time"

View File

@ -37,9 +37,9 @@ use util::ResultExt;
lazy_static::lazy_static! { lazy_static::lazy_static! {
static ref PLAN_LOAD_PATH: Option<PathBuf> = path_env_var("LOAD_PLAN"); static ref PLAN_LOAD_PATH: Option<PathBuf> = path_env_var("LOAD_PLAN");
static ref PLAN_SAVE_PATH: Option<PathBuf> = path_env_var("SAVE_PLAN"); static ref PLAN_SAVE_PATH: Option<PathBuf> = path_env_var("SAVE_PLAN");
static ref LOADED_PLAN_JSON: Mutex<Option<Vec<u8>>> = Default::default();
static ref PLAN: Mutex<Option<Arc<Mutex<TestPlan>>>> = Default::default();
} }
static LOADED_PLAN_JSON: Mutex<Option<Vec<u8>>> = Mutex::new(None);
static PLAN: Mutex<Option<Arc<Mutex<TestPlan>>>> = Mutex::new(None);
#[gpui::test(iterations = 100, on_failure = "on_failure")] #[gpui::test(iterations = 100, on_failure = "on_failure")]
async fn test_random_collaboration( async fn test_random_collaboration(

View File

@ -35,6 +35,7 @@ gpui = { path = "../gpui" }
menu = { path = "../menu" } menu = { path = "../menu" }
picker = { path = "../picker" } picker = { path = "../picker" }
project = { path = "../project" } project = { path = "../project" }
recent_projects = {path = "../recent_projects"}
settings = { path = "../settings" } settings = { path = "../settings" }
theme = { path = "../theme" } theme = { path = "../theme" }
theme_selector = { path = "../theme_selector" } theme_selector = { path = "../theme_selector" }
@ -42,6 +43,7 @@ util = { path = "../util" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
zed-actions = {path = "../zed-actions"} zed-actions = {path = "../zed-actions"}
anyhow.workspace = true anyhow.workspace = true
futures.workspace = true futures.workspace = true
log.workspace = true log.workspace = true

View File

@ -0,0 +1,238 @@
use anyhow::{anyhow, bail};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{elements::*, AppContext, MouseState, Task, ViewContext, ViewHandle};
use picker::{Picker, PickerDelegate, PickerEvent};
use std::{ops::Not, sync::Arc};
use util::ResultExt;
use workspace::{Toast, Workspace};
pub fn init(cx: &mut AppContext) {
Picker::<BranchListDelegate>::init(cx);
}
pub type BranchList = Picker<BranchListDelegate>;
pub fn build_branch_list(
workspace: ViewHandle<Workspace>,
cx: &mut ViewContext<BranchList>,
) -> BranchList {
Picker::new(
BranchListDelegate {
matches: vec![],
workspace,
selected_index: 0,
last_query: String::default(),
},
cx,
)
.with_theme(|theme| theme.picker.clone())
}
pub struct BranchListDelegate {
matches: Vec<StringMatch>,
workspace: ViewHandle<Workspace>,
selected_index: usize,
last_query: String,
}
impl PickerDelegate for BranchListDelegate {
fn placeholder_text(&self) -> Arc<str> {
"Select branch...".into()
}
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
cx.spawn(move |picker, mut cx| async move {
let Some(candidates) = picker
.read_with(&mut cx, |view, cx| {
let delegate = view.delegate();
let project = delegate.workspace.read(cx).project().read(&cx);
let mut cwd =
project
.visible_worktrees(cx)
.next()
.unwrap()
.read(cx)
.abs_path()
.to_path_buf();
cwd.push(".git");
let Some(repo) = project.fs().open_repo(&cwd) else {bail!("Project does not have associated git repository.")};
let mut branches = repo
.lock()
.branches()?;
const RECENT_BRANCHES_COUNT: usize = 10;
if query.is_empty() && 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.unix_timestamp.cmp(&lhs.unix_timestamp)
});
branches.truncate(RECENT_BRANCHES_COUNT);
branches.sort_unstable_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
}
Ok(branches
.iter()
.cloned()
.enumerate()
.map(|(ix, command)| StringMatchCandidate {
id: ix,
char_bag: command.name.chars().collect(),
string: command.name.into(),
})
.collect::<Vec<_>>())
})
.log_err() else { return; };
let Some(candidates) = candidates.log_err() else {return;};
let matches = if query.is_empty() {
candidates
.into_iter()
.enumerate()
.map(|(index, candidate)| StringMatch {
candidate_id: index,
string: candidate.string,
positions: Vec::new(),
score: 0.0,
})
.collect()
} else {
fuzzy::match_strings(
&candidates,
&query,
true,
10000,
&Default::default(),
cx.background(),
)
.await
};
picker
.update(&mut cx, |picker, _| {
let delegate = picker.delegate_mut();
delegate.matches = matches;
if delegate.matches.is_empty() {
delegate.selected_index = 0;
} else {
delegate.selected_index =
core::cmp::min(delegate.selected_index, delegate.matches.len() - 1);
}
delegate.last_query = query;
})
.log_err();
})
}
fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
let current_pick = self.selected_index();
let current_pick = self.matches[current_pick].string.clone();
cx.spawn(|picker, mut cx| async move {
picker.update(&mut cx, |this, cx| {
let project = this.delegate().workspace.read(cx).project().read(cx);
let mut cwd = project
.visible_worktrees(cx)
.next()
.ok_or_else(|| anyhow!("There are no visisible worktrees."))?
.read(cx)
.abs_path()
.to_path_buf();
cwd.push(".git");
let status = project
.fs()
.open_repo(&cwd)
.ok_or_else(|| anyhow!("Could not open repository at path `{}`", cwd.as_os_str().to_string_lossy()))?
.lock()
.change_branch(&current_pick);
if status.is_err() {
const GIT_CHECKOUT_FAILURE_ID: usize = 2048;
this.delegate().workspace.update(cx, |model, ctx| {
model.show_toast(
Toast::new(
GIT_CHECKOUT_FAILURE_ID,
format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"),
),
ctx,
)
});
status?;
}
cx.emit(PickerEvent::Dismiss);
Ok::<(), anyhow::Error>(())
}).log_err();
}).detach();
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
cx.emit(PickerEvent::Dismiss);
}
fn render_match(
&self,
ix: usize,
mouse_state: &mut MouseState,
selected: bool,
cx: &gpui::AppContext,
) -> AnyElement<Picker<Self>> {
const DISPLAYED_MATCH_LEN: usize = 29;
let theme = &theme::current(cx);
let hit = &self.matches[ix];
let shortened_branch_name = util::truncate_and_trailoff(&hit.string, DISPLAYED_MATCH_LEN);
let highlights = hit
.positions
.iter()
.copied()
.filter(|index| index < &DISPLAYED_MATCH_LEN)
.collect();
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
Flex::row()
.with_child(
Label::new(shortened_branch_name.clone(), style.label.clone())
.with_highlights(highlights)
.contained()
.aligned()
.left(),
)
.contained()
.with_style(style.container)
.constrained()
.with_height(theme.contact_finder.row_height)
.into_any()
}
fn render_header(
&self,
cx: &mut ViewContext<Picker<Self>>,
) -> Option<AnyElement<Picker<Self>>> {
let theme = &theme::current(cx);
let style = theme.picker.header.clone();
let label = if self.last_query.is_empty() {
Flex::row()
.with_child(Label::new("Recent branches", style.label.clone()))
.contained()
.with_style(style.container)
} else {
Flex::row()
.with_child(Label::new("Branches", style.label.clone()))
.with_children(self.matches.is_empty().not().then(|| {
let suffix = if self.matches.len() == 1 { "" } else { "es" };
Label::new(
format!("{} match{}", self.matches.len(), suffix),
style.label,
)
.flex_float()
}))
.contained()
.with_style(style.container)
};
Some(label.into_any())
}
}

View File

@ -1,5 +1,8 @@
use crate::{ use crate::{
contact_notification::ContactNotification, contacts_popover, face_pile::FacePile, branch_list::{build_branch_list, BranchList},
contact_notification::ContactNotification,
contacts_popover,
face_pile::FacePile,
toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute,
ToggleScreenSharing, ToggleScreenSharing,
}; };
@ -18,19 +21,25 @@ use gpui::{
AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View, AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View,
ViewContext, ViewHandle, WeakViewHandle, ViewContext, ViewHandle, WeakViewHandle,
}; };
use picker::PickerEvent;
use project::{Project, RepositoryEntry}; use project::{Project, RepositoryEntry};
use recent_projects::{build_recent_projects, RecentProjects};
use std::{ops::Range, sync::Arc}; use std::{ops::Range, sync::Arc};
use theme::{AvatarStyle, Theme}; use theme::{AvatarStyle, Theme};
use util::ResultExt; use util::ResultExt;
use workspace::{FollowNextCollaborator, Workspace}; use workspace::{FollowNextCollaborator, Workspace, WORKSPACE_DB};
// const MAX_TITLE_LENGTH: usize = 75; const MAX_PROJECT_NAME_LENGTH: usize = 40;
const MAX_BRANCH_NAME_LENGTH: usize = 40;
actions!( actions!(
collab, collab,
[ [
ToggleContactsMenu, ToggleContactsMenu,
ToggleUserMenu, ToggleUserMenu,
ToggleVcsMenu,
ToggleProjectMenu,
SwitchBranch,
ShareProject, ShareProject,
UnshareProject, UnshareProject,
] ]
@ -41,6 +50,8 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(CollabTitlebarItem::share_project); cx.add_action(CollabTitlebarItem::share_project);
cx.add_action(CollabTitlebarItem::unshare_project); cx.add_action(CollabTitlebarItem::unshare_project);
cx.add_action(CollabTitlebarItem::toggle_user_menu); cx.add_action(CollabTitlebarItem::toggle_user_menu);
cx.add_action(CollabTitlebarItem::toggle_vcs_menu);
cx.add_action(CollabTitlebarItem::toggle_project_menu);
} }
pub struct CollabTitlebarItem { pub struct CollabTitlebarItem {
@ -49,6 +60,8 @@ pub struct CollabTitlebarItem {
client: Arc<Client>, client: Arc<Client>,
workspace: WeakViewHandle<Workspace>, workspace: WeakViewHandle<Workspace>,
contacts_popover: Option<ViewHandle<ContactsPopover>>, contacts_popover: Option<ViewHandle<ContactsPopover>>,
branch_popover: Option<ViewHandle<BranchList>>,
project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
user_menu: ViewHandle<ContextMenu>, user_menu: ViewHandle<ContextMenu>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
@ -69,12 +82,11 @@ impl View for CollabTitlebarItem {
return Empty::new().into_any(); return Empty::new().into_any();
}; };
let project = self.project.read(cx);
let theme = theme::current(cx).clone(); let theme = theme::current(cx).clone();
let mut left_container = Flex::row(); let mut left_container = Flex::row();
let mut right_container = Flex::row().align_children_center(); let mut right_container = Flex::row().align_children_center();
left_container.add_child(self.collect_title_root_names(&project, theme.clone(), cx)); left_container.add_child(self.collect_title_root_names(theme.clone(), cx));
let user = self.user_store.read(cx).current_user(); let user = self.user_store.read(cx).current_user();
let peer_id = self.client.peer_id(); let peer_id = self.client.peer_id();
@ -182,52 +194,105 @@ impl CollabTitlebarItem {
menu.set_position_mode(OverlayPositionMode::Local); menu.set_position_mode(OverlayPositionMode::Local);
menu menu
}), }),
branch_popover: None,
project_popover: None,
_subscriptions: subscriptions, _subscriptions: subscriptions,
} }
} }
fn collect_title_root_names( fn collect_title_root_names(
&self, &self,
project: &Project,
theme: Arc<Theme>, theme: Arc<Theme>,
cx: &ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> AnyElement<Self> { ) -> AnyElement<Self> {
let project = self.project.read(cx);
let (name, entry) = {
let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| { let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
let worktree = worktree.read(cx); let worktree = worktree.read(cx);
(worktree.root_name(), worktree.root_git_entry()) (worktree.root_name(), worktree.root_git_entry())
}); });
let (name, entry) = names_and_branches.next().unwrap_or(("", None)); names_and_branches.next().unwrap_or(("", None))
};
let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
let branch_prepended = entry let branch_prepended = entry
.as_ref() .as_ref()
.and_then(RepositoryEntry::branch) .and_then(RepositoryEntry::branch)
.map(|branch| format!("/{branch}")); .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH));
let text_style = theme.titlebar.title.clone(); let project_style = theme.titlebar.project_menu_button.clone();
let git_style = theme.titlebar.git_menu_button.clone();
let divider_style = theme.titlebar.project_name_divider.clone();
let item_spacing = theme.titlebar.item_spacing; let item_spacing = theme.titlebar.item_spacing;
let mut highlight = text_style.clone();
highlight.color = theme.titlebar.highlight_color;
let style = LabelStyle {
text: text_style,
highlight_text: Some(highlight),
};
let mut ret = Flex::row().with_child( let mut ret = Flex::row().with_child(
Label::new(name.to_owned(), style.clone()) Stack::new()
.with_highlights((0..name.len()).into_iter().collect()) .with_child(
MouseEventHandler::<ToggleProjectMenu, Self>::new(0, cx, |mouse_state, _| {
let style = project_style
.in_state(self.project_popover.is_some())
.style_for(mouse_state);
Label::new(name, style.text.clone())
.contained() .contained()
.with_style(style.container)
.aligned() .aligned()
.left() .left()
.into_any_named("title-project-name"), .into_any_named("title-project-name")
})
.with_cursor_style(CursorStyle::PointingHand)
.on_down(MouseButton::Left, move |_, this, cx| {
this.toggle_project_menu(&Default::default(), cx)
})
.on_click(MouseButton::Left, move |_, _, _| {}),
)
.with_children(self.render_project_popover_host(&theme.titlebar, cx)),
); );
if let Some(git_branch) = branch_prepended { if let Some(git_branch) = branch_prepended {
ret = ret.with_child( ret = ret.with_child(
Label::new(git_branch, style) Flex::row()
.with_child(
Label::new("/", divider_style.text)
.contained() .contained()
.with_style(divider_style.container)
.aligned()
.left(),
)
.with_child(
Stack::new()
.with_child(
MouseEventHandler::<ToggleVcsMenu, Self>::new(
0,
cx,
|mouse_state, cx| {
enum BranchPopoverTooltip {}
let style = git_style
.in_state(self.branch_popover.is_some())
.style_for(mouse_state);
Label::new(git_branch, style.text.clone())
.contained()
.with_style(style.container.clone())
.with_margin_right(item_spacing) .with_margin_right(item_spacing)
.aligned() .aligned()
.left() .left()
.into_any_named("title-project-branch"), .with_tooltip::<BranchPopoverTooltip>(
0,
"Recent branches".into(),
None,
theme.tooltip.clone(),
cx,
)
.into_any_named("title-project-branch")
},
)
.with_cursor_style(CursorStyle::PointingHand)
.on_down(MouseButton::Left, move |_, this, cx| {
this.toggle_vcs_menu(&Default::default(), cx)
})
.on_click(MouseButton::Left, move |_, _, _| {}),
)
.with_children(self.render_branches_popover_host(&theme.titlebar, cx)),
),
) )
} }
ret.into_any() ret.into_any()
@ -317,10 +382,138 @@ impl CollabTitlebarItem {
), ),
] ]
}; };
user_menu.show(Default::default(), AnchorCorner::TopRight, items, cx); user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
}); });
} }
fn render_branches_popover_host<'a>(
&'a self,
_theme: &'a theme::Titlebar,
cx: &'a mut ViewContext<Self>,
) -> Option<AnyElement<Self>> {
self.branch_popover.as_ref().map(|child| {
let theme = theme::current(cx).clone();
let child = ChildView::new(child, cx);
let child = MouseEventHandler::<BranchList, Self>::new(0, cx, |_, _| {
child
.flex(1., true)
.contained()
.constrained()
.with_width(theme.contacts_popover.width)
.with_height(theme.contacts_popover.height)
})
.on_click(MouseButton::Left, |_, _, _| {})
.on_down_out(MouseButton::Left, move |_, this, cx| {
this.branch_popover.take();
cx.emit(());
cx.notify();
})
.contained()
.into_any();
Overlay::new(child)
.with_fit_mode(OverlayFitMode::SwitchAnchor)
.with_anchor_corner(AnchorCorner::TopLeft)
.with_z_index(999)
.aligned()
.bottom()
.left()
.into_any()
})
}
fn render_project_popover_host<'a>(
&'a self,
_theme: &'a theme::Titlebar,
cx: &'a mut ViewContext<Self>,
) -> Option<AnyElement<Self>> {
self.project_popover.as_ref().map(|child| {
let theme = theme::current(cx).clone();
let child = ChildView::new(child, cx);
let child = MouseEventHandler::<RecentProjects, Self>::new(0, cx, |_, _| {
child
.flex(1., true)
.contained()
.constrained()
.with_width(theme.contacts_popover.width)
.with_height(theme.contacts_popover.height)
})
.on_click(MouseButton::Left, |_, _, _| {})
.on_down_out(MouseButton::Left, move |_, this, cx| {
this.project_popover.take();
cx.emit(());
cx.notify();
})
.into_any();
Overlay::new(child)
.with_fit_mode(OverlayFitMode::SwitchAnchor)
.with_anchor_corner(AnchorCorner::TopLeft)
.with_z_index(999)
.aligned()
.bottom()
.left()
.into_any()
})
}
pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
if self.branch_popover.take().is_none() {
if let Some(workspace) = self.workspace.upgrade(cx) {
let view = cx.add_view(|cx| build_branch_list(workspace, cx));
cx.subscribe(&view, |this, _, event, cx| {
match event {
PickerEvent::Dismiss => {
this.branch_popover = None;
}
}
cx.notify();
})
.detach();
self.project_popover.take();
cx.focus(&view);
self.branch_popover = Some(view);
}
}
cx.notify();
}
pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) {
let workspace = self.workspace.clone();
if self.project_popover.take().is_none() {
cx.spawn(|this, mut cx| async move {
let workspaces = WORKSPACE_DB
.recent_workspaces_on_disk()
.await
.unwrap_or_default()
.into_iter()
.map(|(_, location)| location)
.collect();
let workspace = workspace.clone();
this.update(&mut cx, move |this, cx| {
let view = cx.add_view(|cx| build_recent_projects(workspace, workspaces, cx));
cx.subscribe(&view, |this, _, event, cx| {
match event {
PickerEvent::Dismiss => {
this.project_popover = None;
}
}
cx.notify();
})
.detach();
cx.focus(&view);
this.branch_popover.take();
this.project_popover = Some(view);
cx.notify();
})
.log_err();
})
.detach();
}
cx.notify();
}
fn render_toggle_contacts_button( fn render_toggle_contacts_button(
&self, &self,
theme: &Theme, theme: &Theme,
@ -683,6 +876,9 @@ impl CollabTitlebarItem {
.into_any() .into_any()
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_down(MouseButton::Left, move |_, this, cx| {
this.user_menu.update(cx, |menu, _| menu.delay_cancel());
})
.on_click(MouseButton::Left, move |_, this, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
this.toggle_user_menu(&Default::default(), cx) this.toggle_user_menu(&Default::default(), cx)
}) })
@ -730,7 +926,7 @@ impl CollabTitlebarItem {
self.contacts_popover.as_ref().map(|popover| { self.contacts_popover.as_ref().map(|popover| {
Overlay::new(ChildView::new(popover, cx)) Overlay::new(ChildView::new(popover, cx))
.with_fit_mode(OverlayFitMode::SwitchAnchor) .with_fit_mode(OverlayFitMode::SwitchAnchor)
.with_anchor_corner(AnchorCorner::TopRight) .with_anchor_corner(AnchorCorner::TopLeft)
.with_z_index(999) .with_z_index(999)
.aligned() .aligned()
.bottom() .bottom()

View File

@ -1,3 +1,4 @@
mod branch_list;
mod collab_titlebar_item; mod collab_titlebar_item;
mod contact_finder; mod contact_finder;
mod contact_list; mod contact_list;
@ -28,6 +29,7 @@ actions!(
); );
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) { pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
branch_list::init(cx);
collab_titlebar_item::init(cx); collab_titlebar_item::init(cx);
contact_list::init(cx); contact_list::init(cx);
contact_finder::init(cx); contact_finder::init(cx);

View File

@ -124,6 +124,7 @@ pub struct ContextMenu {
items: Vec<ContextMenuItem>, items: Vec<ContextMenuItem>,
selected_index: Option<usize>, selected_index: Option<usize>,
visible: bool, visible: bool,
delay_cancel: bool,
previously_focused_view_id: Option<usize>, previously_focused_view_id: Option<usize>,
parent_view_id: usize, parent_view_id: usize,
_actions_observation: Subscription, _actions_observation: Subscription,
@ -178,6 +179,7 @@ impl ContextMenu {
pub fn new(parent_view_id: usize, cx: &mut ViewContext<Self>) -> Self { pub fn new(parent_view_id: usize, cx: &mut ViewContext<Self>) -> Self {
Self { Self {
show_count: 0, show_count: 0,
delay_cancel: false,
anchor_position: Default::default(), anchor_position: Default::default(),
anchor_corner: AnchorCorner::TopLeft, anchor_corner: AnchorCorner::TopLeft,
position_mode: OverlayPositionMode::Window, position_mode: OverlayPositionMode::Window,
@ -232,15 +234,22 @@ impl ContextMenu {
} }
} }
pub fn delay_cancel(&mut self) {
self.delay_cancel = true;
}
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) { fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
if !self.delay_cancel {
self.reset(cx); self.reset(cx);
let show_count = self.show_count; let show_count = self.show_count;
cx.defer(move |this, cx| { cx.defer(move |this, cx| {
if cx.handle().is_focused(cx) && this.show_count == show_count { if cx.handle().is_focused(cx) && this.show_count == show_count {
let window_id = cx.window_id(); (**cx).focus(this.previously_focused_view_id.take());
(**cx).focus(window_id, this.previously_focused_view_id.take());
} }
}); });
} else {
self.delay_cancel = false;
}
} }
fn reset(&mut self, cx: &mut ViewContext<Self>) { fn reset(&mut self, cx: &mut ViewContext<Self>) {
@ -293,6 +302,34 @@ impl ContextMenu {
} }
} }
pub fn toggle(
&mut self,
anchor_position: Vector2F,
anchor_corner: AnchorCorner,
items: Vec<ContextMenuItem>,
cx: &mut ViewContext<Self>,
) {
if self.visible() {
self.cancel(&Cancel, cx);
} else {
let mut items = items.into_iter().peekable();
if items.peek().is_some() {
self.items = items.collect();
self.anchor_position = anchor_position;
self.anchor_corner = anchor_corner;
self.visible = true;
self.show_count += 1;
if !cx.is_self_focused() {
self.previously_focused_view_id = cx.focused_view_id();
}
cx.focus_self();
} else {
self.visible = false;
}
}
cx.notify();
}
pub fn show( pub fn show(
&mut self, &mut self,
anchor_position: Vector2F, anchor_position: Vector2F,

View File

@ -102,6 +102,9 @@ impl View for CopilotButton {
} }
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_down(MouseButton::Left, |_, this, cx| {
this.popup_menu.update(cx, |menu, _| menu.delay_cancel());
})
.on_click(MouseButton::Left, { .on_click(MouseButton::Left, {
let status = status.clone(); let status = status.clone();
move |_, this, cx| match status { move |_, this, cx| match status {
@ -186,7 +189,7 @@ impl CopilotButton {
})); }));
self.popup_menu.update(cx, |menu, cx| { self.popup_menu.update(cx, |menu, cx| {
menu.show( menu.toggle(
Default::default(), Default::default(),
AnchorCorner::BottomRight, AnchorCorner::BottomRight,
menu_options, menu_options,
@ -266,7 +269,7 @@ impl CopilotButton {
menu_options.push(ContextMenuItem::action("Sign Out", SignOut)); menu_options.push(ContextMenuItem::action("Sign Out", SignOut));
self.popup_menu.update(cx, |menu, cx| { self.popup_menu.update(cx, |menu, cx| {
menu.show( menu.toggle(
Default::default(), Default::default(),
AnchorCorner::BottomRight, AnchorCorner::BottomRight,
menu_options, menu_options,

View File

@ -41,12 +41,11 @@ const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
const DB_FILE_NAME: &'static str = "db.sqlite"; const DB_FILE_NAME: &'static str = "db.sqlite";
lazy_static::lazy_static! { lazy_static::lazy_static! {
// !!!!!!! CHANGE BACK TO DEFAULT FALSE BEFORE SHIPPING pub static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
static ref DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None); pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false); pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
} }
static DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
/// Open or create a database at the given directory path. /// Open or create a database at the given directory path.
/// This will retry a couple times if there are failures. If opening fails once, the db directory /// This will retry a couple times if there are failures. If opening fails once, the db directory

View File

@ -20,7 +20,6 @@ use language::{
use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc}; use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
use sum_tree::{Bias, TreeMap}; use sum_tree::{Bias, TreeMap};
use tab_map::TabMap; use tab_map::TabMap;
use text::Rope;
use wrap_map::WrapMap; use wrap_map::WrapMap;
pub use block_map::{ pub use block_map::{
@ -28,7 +27,7 @@ pub use block_map::{
BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock, BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
}; };
pub use self::inlay_map::{Inlay, InlayProperties}; pub use self::inlay_map::Inlay;
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum FoldStatus { pub enum FoldStatus {
@ -246,10 +245,10 @@ impl DisplayMap {
self.inlay_map.current_inlays() self.inlay_map.current_inlays()
} }
pub fn splice_inlays<T: Into<Rope>>( pub fn splice_inlays(
&mut self, &mut self,
to_remove: Vec<InlayId>, to_remove: Vec<InlayId>,
to_insert: Vec<(InlayId, InlayProperties<T>)>, to_insert: Vec<Inlay>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
if to_remove.is_empty() && to_insert.is_empty() { if to_remove.is_empty() && to_insert.is_empty() {

View File

@ -2,9 +2,9 @@ use crate::{
multi_buffer::{MultiBufferChunks, MultiBufferRows}, multi_buffer::{MultiBufferChunks, MultiBufferRows},
Anchor, InlayId, MultiBufferSnapshot, ToOffset, Anchor, InlayId, MultiBufferSnapshot, ToOffset,
}; };
use collections::{BTreeMap, BTreeSet, HashMap}; use collections::{BTreeMap, BTreeSet};
use gpui::fonts::HighlightStyle; use gpui::fonts::HighlightStyle;
use language::{Chunk, Edit, Point, Rope, TextSummary}; use language::{Chunk, Edit, Point, TextSummary};
use std::{ use std::{
any::TypeId, any::TypeId,
cmp, cmp,
@ -13,13 +13,12 @@ use std::{
vec, vec,
}; };
use sum_tree::{Bias, Cursor, SumTree}; use sum_tree::{Bias, Cursor, SumTree};
use text::Patch; use text::{Patch, Rope};
use super::TextHighlights; use super::TextHighlights;
pub struct InlayMap { pub struct InlayMap {
snapshot: InlaySnapshot, snapshot: InlaySnapshot,
inlays_by_id: HashMap<InlayId, Inlay>,
inlays: Vec<Inlay>, inlays: Vec<Inlay>,
} }
@ -43,10 +42,29 @@ pub struct Inlay {
pub text: text::Rope, pub text: text::Rope,
} }
#[derive(Debug, Clone)] impl Inlay {
pub struct InlayProperties<T> { pub fn hint(id: usize, position: Anchor, hint: &project::InlayHint) -> Self {
pub position: Anchor, let mut text = hint.text();
pub text: T, if hint.padding_right && !text.ends_with(' ') {
text.push(' ');
}
if hint.padding_left && !text.starts_with(' ') {
text.insert(0, ' ');
}
Self {
id: InlayId::Hint(id),
position,
text: text.into(),
}
}
pub fn suggestion<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
Self {
id: InlayId::Suggestion(id),
position,
text: text.into(),
}
}
} }
impl sum_tree::Item for Transform { impl sum_tree::Item for Transform {
@ -368,7 +386,6 @@ impl InlayMap {
( (
Self { Self {
snapshot: snapshot.clone(), snapshot: snapshot.clone(),
inlays_by_id: HashMap::default(),
inlays: Vec::new(), inlays: Vec::new(),
}, },
snapshot, snapshot,
@ -510,45 +527,40 @@ impl InlayMap {
} }
} }
pub fn splice<T: Into<Rope>>( pub fn splice(
&mut self, &mut self,
to_remove: Vec<InlayId>, to_remove: Vec<InlayId>,
to_insert: Vec<(InlayId, InlayProperties<T>)>, to_insert: Vec<Inlay>,
) -> (InlaySnapshot, Vec<InlayEdit>) { ) -> (InlaySnapshot, Vec<InlayEdit>) {
let snapshot = &mut self.snapshot; let snapshot = &mut self.snapshot;
let mut edits = BTreeSet::new(); let mut edits = BTreeSet::new();
self.inlays.retain(|inlay| !to_remove.contains(&inlay.id)); self.inlays.retain(|inlay| {
for inlay_id in to_remove { let retain = !to_remove.contains(&inlay.id);
if let Some(inlay) = self.inlays_by_id.remove(&inlay_id) { if !retain {
let offset = inlay.position.to_offset(&snapshot.buffer); let offset = inlay.position.to_offset(&snapshot.buffer);
edits.insert(offset); edits.insert(offset);
} }
} retain
});
for (existing_id, properties) in to_insert {
let inlay = Inlay {
id: existing_id,
position: properties.position,
text: properties.text.into(),
};
for inlay_to_insert in to_insert {
// Avoid inserting empty inlays. // Avoid inserting empty inlays.
if inlay.text.is_empty() { if inlay_to_insert.text.is_empty() {
continue; continue;
} }
self.inlays_by_id.insert(inlay.id, inlay.clone()); let offset = inlay_to_insert.position.to_offset(&snapshot.buffer);
match self match self.inlays.binary_search_by(|probe| {
.inlays probe
.binary_search_by(|probe| probe.position.cmp(&inlay.position, &snapshot.buffer)) .position
{ .cmp(&inlay_to_insert.position, &snapshot.buffer)
}) {
Ok(ix) | Err(ix) => { Ok(ix) | Err(ix) => {
self.inlays.insert(ix, inlay.clone()); self.inlays.insert(ix, inlay_to_insert);
} }
} }
let offset = inlay.position.to_offset(&snapshot.buffer);
edits.insert(offset); edits.insert(offset);
} }
@ -606,15 +618,19 @@ impl InlayMap {
} else { } else {
InlayId::Suggestion(post_inc(next_inlay_id)) InlayId::Suggestion(post_inc(next_inlay_id))
}; };
to_insert.push(( to_insert.push(Inlay {
inlay_id, id: inlay_id,
InlayProperties {
position: snapshot.buffer.anchor_at(position, bias), position: snapshot.buffer.anchor_at(position, bias),
text, text: text.into(),
}, });
));
} else { } else {
to_remove.push(*self.inlays_by_id.keys().choose(rng).unwrap()); to_remove.push(
self.inlays
.iter()
.choose(rng)
.map(|inlay| inlay.id)
.unwrap(),
);
} }
} }
log::info!("removing inlays: {:?}", to_remove); log::info!("removing inlays: {:?}", to_remove);
@ -1095,6 +1111,7 @@ mod tests {
use super::*; use super::*;
use crate::{InlayId, MultiBuffer}; use crate::{InlayId, MultiBuffer};
use gpui::AppContext; use gpui::AppContext;
use project::{InlayHint, InlayHintLabel};
use rand::prelude::*; use rand::prelude::*;
use settings::SettingsStore; use settings::SettingsStore;
use std::{cmp::Reverse, env, sync::Arc}; use std::{cmp::Reverse, env, sync::Arc};
@ -1102,6 +1119,89 @@ mod tests {
use text::Patch; use text::Patch;
use util::post_inc; use util::post_inc;
#[test]
fn test_inlay_properties_label_padding() {
assert_eq!(
Inlay::hint(
0,
Anchor::min(),
&InlayHint {
label: InlayHintLabel::String("a".to_string()),
buffer_id: 0,
position: text::Anchor::default(),
padding_left: false,
padding_right: false,
tooltip: None,
kind: None,
},
)
.text
.to_string(),
"a",
"Should not pad label if not requested"
);
assert_eq!(
Inlay::hint(
0,
Anchor::min(),
&InlayHint {
label: InlayHintLabel::String("a".to_string()),
buffer_id: 0,
position: text::Anchor::default(),
padding_left: true,
padding_right: true,
tooltip: None,
kind: None,
},
)
.text
.to_string(),
" a ",
"Should pad label for every side requested"
);
assert_eq!(
Inlay::hint(
0,
Anchor::min(),
&InlayHint {
label: InlayHintLabel::String(" a ".to_string()),
buffer_id: 0,
position: text::Anchor::default(),
padding_left: false,
padding_right: false,
tooltip: None,
kind: None,
},
)
.text
.to_string(),
" a ",
"Should not change already padded label"
);
assert_eq!(
Inlay::hint(
0,
Anchor::min(),
&InlayHint {
label: InlayHintLabel::String(" a ".to_string()),
buffer_id: 0,
position: text::Anchor::default(),
padding_left: true,
padding_right: true,
tooltip: None,
kind: None,
},
)
.text
.to_string(),
" a ",
"Should not change already padded label"
);
}
#[gpui::test] #[gpui::test]
fn test_basic_inlays(cx: &mut AppContext) { fn test_basic_inlays(cx: &mut AppContext) {
let buffer = MultiBuffer::build_simple("abcdefghi", cx); let buffer = MultiBuffer::build_simple("abcdefghi", cx);
@ -1112,13 +1212,11 @@ mod tests {
let (inlay_snapshot, _) = inlay_map.splice( let (inlay_snapshot, _) = inlay_map.splice(
Vec::new(), Vec::new(),
vec![( vec![Inlay {
InlayId::Hint(post_inc(&mut next_inlay_id)), id: InlayId::Hint(post_inc(&mut next_inlay_id)),
InlayProperties {
position: buffer.read(cx).snapshot(cx).anchor_after(3), position: buffer.read(cx).snapshot(cx).anchor_after(3),
text: "|123|", text: "|123|".into(),
}, }],
)],
); );
assert_eq!(inlay_snapshot.text(), "abc|123|defghi"); assert_eq!(inlay_snapshot.text(), "abc|123|defghi");
assert_eq!( assert_eq!(
@ -1191,20 +1289,16 @@ mod tests {
let (inlay_snapshot, _) = inlay_map.splice( let (inlay_snapshot, _) = inlay_map.splice(
Vec::new(), Vec::new(),
vec![ vec![
( Inlay {
InlayId::Hint(post_inc(&mut next_inlay_id)), id: InlayId::Hint(post_inc(&mut next_inlay_id)),
InlayProperties {
position: buffer.read(cx).snapshot(cx).anchor_before(3), position: buffer.read(cx).snapshot(cx).anchor_before(3),
text: "|123|", text: "|123|".into(),
}, },
), Inlay {
( id: InlayId::Suggestion(post_inc(&mut next_inlay_id)),
InlayId::Suggestion(post_inc(&mut next_inlay_id)),
InlayProperties {
position: buffer.read(cx).snapshot(cx).anchor_after(3), position: buffer.read(cx).snapshot(cx).anchor_after(3),
text: "|456|", text: "|456|".into(),
}, },
),
], ],
); );
assert_eq!(inlay_snapshot.text(), "abx|123||456|yDzefghi"); assert_eq!(inlay_snapshot.text(), "abx|123||456|yDzefghi");
@ -1389,8 +1483,10 @@ mod tests {
); );
// The inlays can be manually removed. // The inlays can be manually removed.
let (inlay_snapshot, _) = inlay_map let (inlay_snapshot, _) = inlay_map.splice(
.splice::<String>(inlay_map.inlays_by_id.keys().copied().collect(), Vec::new()); inlay_map.inlays.iter().map(|inlay| inlay.id).collect(),
Vec::new(),
);
assert_eq!(inlay_snapshot.text(), "abxJKLyDzefghi"); assert_eq!(inlay_snapshot.text(), "abxJKLyDzefghi");
} }
@ -1404,27 +1500,21 @@ mod tests {
let (inlay_snapshot, _) = inlay_map.splice( let (inlay_snapshot, _) = inlay_map.splice(
Vec::new(), Vec::new(),
vec![ vec![
( Inlay {
InlayId::Hint(post_inc(&mut next_inlay_id)), id: InlayId::Hint(post_inc(&mut next_inlay_id)),
InlayProperties {
position: buffer.read(cx).snapshot(cx).anchor_before(0), position: buffer.read(cx).snapshot(cx).anchor_before(0),
text: "|123|\n", text: "|123|\n".into(),
}, },
), Inlay {
( id: InlayId::Hint(post_inc(&mut next_inlay_id)),
InlayId::Hint(post_inc(&mut next_inlay_id)),
InlayProperties {
position: buffer.read(cx).snapshot(cx).anchor_before(4), position: buffer.read(cx).snapshot(cx).anchor_before(4),
text: "|456|", text: "|456|".into(),
}, },
), Inlay {
( id: InlayId::Suggestion(post_inc(&mut next_inlay_id)),
InlayId::Suggestion(post_inc(&mut next_inlay_id)),
InlayProperties {
position: buffer.read(cx).snapshot(cx).anchor_before(7), position: buffer.read(cx).snapshot(cx).anchor_before(7),
text: "\n|567|\n", text: "\n|567|\n".into(),
}, },
),
], ],
); );
assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi"); assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi");
@ -1514,7 +1604,7 @@ mod tests {
(offset, inlay.clone()) (offset, inlay.clone())
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut expected_text = Rope::from(buffer_snapshot.text().as_str()); let mut expected_text = Rope::from(buffer_snapshot.text());
for (offset, inlay) in inlays.into_iter().rev() { for (offset, inlay) in inlays.into_iter().rev() {
expected_text.replace(offset..offset, &inlay.text.to_string()); expected_text.replace(offset..offset, &inlay.text.to_string());
} }

View File

@ -26,7 +26,7 @@ use aho_corasick::AhoCorasick;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use blink_manager::BlinkManager; use blink_manager::BlinkManager;
use client::{ClickhouseEvent, TelemetrySettings}; use client::{ClickhouseEvent, TelemetrySettings};
use clock::ReplicaId; use clock::{Global, ReplicaId};
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
use copilot::Copilot; use copilot::Copilot;
pub use display_map::DisplayPoint; pub use display_map::DisplayPoint;
@ -190,6 +190,15 @@ pub enum InlayId {
Hint(usize), Hint(usize),
} }
impl InlayId {
fn id(&self) -> usize {
match self {
Self::Suggestion(id) => *id,
Self::Hint(id) => *id,
}
}
}
actions!( actions!(
editor, editor,
[ [
@ -1195,11 +1204,11 @@ enum GotoDefinitionKind {
Type, Type,
} }
#[derive(Debug, Copy, Clone)] #[derive(Debug, Clone)]
enum InlayRefreshReason { enum InlayRefreshReason {
SettingsChange(InlayHintSettings), SettingsChange(InlayHintSettings),
NewLinesShown, NewLinesShown,
ExcerptEdited, BufferEdited(HashSet<Arc<Language>>),
RefreshRequested, RefreshRequested,
} }
@ -2026,6 +2035,7 @@ impl Editor {
} }
let selections = self.selections.all_adjusted(cx); let selections = self.selections.all_adjusted(cx);
let mut brace_inserted = false;
let mut edits = Vec::new(); let mut edits = Vec::new();
let mut new_selections = Vec::with_capacity(selections.len()); let mut new_selections = Vec::with_capacity(selections.len());
let mut new_autoclose_regions = Vec::new(); let mut new_autoclose_regions = Vec::new();
@ -2084,6 +2094,7 @@ impl Editor {
selection.range(), selection.range(),
format!("{}{}", text, bracket_pair.end).into(), format!("{}{}", text, bracket_pair.end).into(),
)); ));
brace_inserted = true;
continue; continue;
} }
} }
@ -2110,6 +2121,7 @@ impl Editor {
selection.end..selection.end, selection.end..selection.end,
bracket_pair.end.as_str().into(), bracket_pair.end.as_str().into(),
)); ));
brace_inserted = true;
new_selections.push(( new_selections.push((
Selection { Selection {
id: selection.id, id: selection.id,
@ -2177,8 +2189,7 @@ impl Editor {
let had_active_copilot_suggestion = this.has_active_copilot_suggestion(cx); let had_active_copilot_suggestion = this.has_active_copilot_suggestion(cx);
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections)); this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
// When buffer contents is updated and caret is moved, try triggering on type formatting. if !brace_inserted && settings::get::<EditorSettings>(cx).use_on_type_format {
if settings::get::<EditorSettings>(cx).use_on_type_format {
if let Some(on_type_format_task) = if let Some(on_type_format_task) =
this.trigger_on_type_formatting(text.to_string(), cx) this.trigger_on_type_formatting(text.to_string(), cx)
{ {
@ -2617,7 +2628,7 @@ impl Editor {
return; return;
} }
let invalidate_cache = match reason { let (invalidate_cache, required_languages) = match reason {
InlayRefreshReason::SettingsChange(new_settings) => { InlayRefreshReason::SettingsChange(new_settings) => {
match self.inlay_hint_cache.update_settings( match self.inlay_hint_cache.update_settings(
&self.buffer, &self.buffer,
@ -2633,16 +2644,18 @@ impl Editor {
return; return;
} }
ControlFlow::Break(None) => return, ControlFlow::Break(None) => return,
ControlFlow::Continue(()) => InvalidationStrategy::RefreshRequested, ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None),
} }
} }
InlayRefreshReason::NewLinesShown => InvalidationStrategy::None, InlayRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
InlayRefreshReason::ExcerptEdited => InvalidationStrategy::ExcerptEdited, InlayRefreshReason::BufferEdited(buffer_languages) => {
InlayRefreshReason::RefreshRequested => InvalidationStrategy::RefreshRequested, (InvalidationStrategy::BufferEdited, Some(buffer_languages))
}
InlayRefreshReason::RefreshRequested => (InvalidationStrategy::RefreshRequested, None),
}; };
self.inlay_hint_cache.refresh_inlay_hints( self.inlay_hint_cache.refresh_inlay_hints(
self.excerpt_visible_offsets(cx), self.excerpt_visible_offsets(required_languages.as_ref(), cx),
invalidate_cache, invalidate_cache,
cx, cx,
) )
@ -2661,8 +2674,9 @@ impl Editor {
fn excerpt_visible_offsets( fn excerpt_visible_offsets(
&self, &self,
restrict_to_languages: Option<&HashSet<Arc<Language>>>,
cx: &mut ViewContext<'_, '_, Editor>, cx: &mut ViewContext<'_, '_, Editor>,
) -> HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)> { ) -> HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)> {
let multi_buffer = self.buffer().read(cx); let multi_buffer = self.buffer().read(cx);
let multi_buffer_snapshot = multi_buffer.snapshot(cx); let multi_buffer_snapshot = multi_buffer.snapshot(cx);
let multi_buffer_visible_start = self let multi_buffer_visible_start = self
@ -2680,8 +2694,22 @@ impl Editor {
.range_to_buffer_ranges(multi_buffer_visible_range, cx) .range_to_buffer_ranges(multi_buffer_visible_range, cx)
.into_iter() .into_iter()
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
.map(|(buffer, excerpt_visible_range, excerpt_id)| { .filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| {
(excerpt_id, (buffer, excerpt_visible_range)) let buffer = buffer_handle.read(cx);
let language = buffer.language()?;
if let Some(restrict_to_languages) = restrict_to_languages {
if !restrict_to_languages.contains(language) {
return None;
}
}
Some((
excerpt_id,
(
buffer_handle,
buffer.version().clone(),
excerpt_visible_range,
),
))
}) })
.collect() .collect()
} }
@ -2689,26 +2717,11 @@ impl Editor {
fn splice_inlay_hints( fn splice_inlay_hints(
&self, &self,
to_remove: Vec<InlayId>, to_remove: Vec<InlayId>,
to_insert: Vec<(Anchor, InlayId, project::InlayHint)>, to_insert: Vec<Inlay>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
let buffer = self.buffer.read(cx).read(cx);
let new_inlays = to_insert
.into_iter()
.map(|(position, id, hint)| {
let mut text = hint.text();
if hint.padding_right {
text.push(' ');
}
if hint.padding_left {
text.insert(0, ' ');
}
(id, InlayProperties { position, text })
})
.collect();
drop(buffer);
self.display_map.update(cx, |display_map, cx| { self.display_map.update(cx, |display_map, cx| {
display_map.splice_inlays(to_remove, new_inlays, cx); display_map.splice_inlays(to_remove, to_insert, cx);
}); });
} }
@ -3393,7 +3406,7 @@ impl Editor {
} }
self.display_map.update(cx, |map, cx| { self.display_map.update(cx, |map, cx| {
map.splice_inlays::<&str>(vec![suggestion.id], Vec::new(), cx) map.splice_inlays(vec![suggestion.id], Vec::new(), cx)
}); });
cx.notify(); cx.notify();
true true
@ -3426,7 +3439,7 @@ impl Editor {
fn take_active_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Inlay> { fn take_active_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Inlay> {
let suggestion = self.copilot_state.suggestion.take()?; let suggestion = self.copilot_state.suggestion.take()?;
self.display_map.update(cx, |map, cx| { self.display_map.update(cx, |map, cx| {
map.splice_inlays::<&str>(vec![suggestion.id], Default::default(), cx); map.splice_inlays(vec![suggestion.id], Default::default(), cx);
}); });
let buffer = self.buffer.read(cx).read(cx); let buffer = self.buffer.read(cx).read(cx);
@ -3457,21 +3470,11 @@ impl Editor {
to_remove.push(suggestion.id); to_remove.push(suggestion.id);
} }
let suggestion_inlay_id = InlayId::Suggestion(post_inc(&mut self.next_inlay_id)); let suggestion_inlay =
let to_insert = vec![( Inlay::suggestion(post_inc(&mut self.next_inlay_id), cursor, text);
suggestion_inlay_id, self.copilot_state.suggestion = Some(suggestion_inlay.clone());
InlayProperties {
position: cursor,
text: text.clone(),
},
)];
self.display_map.update(cx, move |map, cx| { self.display_map.update(cx, move |map, cx| {
map.splice_inlays(to_remove, to_insert, cx) map.splice_inlays(to_remove, vec![suggestion_inlay], cx)
});
self.copilot_state.suggestion = Some(Inlay {
id: suggestion_inlay_id,
position: cursor,
text,
}); });
cx.notify(); cx.notify();
} else { } else {
@ -5120,7 +5123,7 @@ impl Editor {
self.change_selections(Some(Autoscroll::fit()), cx, |s| { self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| { s.move_with(|map, selection| {
selection.collapse_to( selection.collapse_to(
movement::start_of_paragraph(map, selection.head()), movement::start_of_paragraph(map, selection.head(), 1),
SelectionGoal::None, SelectionGoal::None,
) )
}); });
@ -5140,7 +5143,7 @@ impl Editor {
self.change_selections(Some(Autoscroll::fit()), cx, |s| { self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| { s.move_with(|map, selection| {
selection.collapse_to( selection.collapse_to(
movement::end_of_paragraph(map, selection.head()), movement::end_of_paragraph(map, selection.head(), 1),
SelectionGoal::None, SelectionGoal::None,
) )
}); });
@ -5159,7 +5162,10 @@ impl Editor {
self.change_selections(Some(Autoscroll::fit()), cx, |s| { self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_heads_with(|map, head, _| { s.move_heads_with(|map, head, _| {
(movement::start_of_paragraph(map, head), SelectionGoal::None) (
movement::start_of_paragraph(map, head, 1),
SelectionGoal::None,
)
}); });
}) })
} }
@ -5176,7 +5182,10 @@ impl Editor {
self.change_selections(Some(Autoscroll::fit()), cx, |s| { self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_heads_with(|map, head, _| { s.move_heads_with(|map, head, _| {
(movement::end_of_paragraph(map, head), SelectionGoal::None) (
movement::end_of_paragraph(map, head, 1),
SelectionGoal::None,
)
}); });
}) })
} }
@ -7256,7 +7265,7 @@ impl Editor {
fn on_buffer_event( fn on_buffer_event(
&mut self, &mut self,
_: ModelHandle<MultiBuffer>, multibuffer: ModelHandle<MultiBuffer>,
event: &multi_buffer::Event, event: &multi_buffer::Event,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
@ -7268,7 +7277,33 @@ impl Editor {
self.update_visible_copilot_suggestion(cx); self.update_visible_copilot_suggestion(cx);
} }
cx.emit(Event::BufferEdited); cx.emit(Event::BufferEdited);
self.refresh_inlays(InlayRefreshReason::ExcerptEdited, cx);
if let Some(project) = &self.project {
let project = project.read(cx);
let languages_affected = multibuffer
.read(cx)
.all_buffers()
.into_iter()
.filter_map(|buffer| {
let buffer = buffer.read(cx);
let language = buffer.language()?;
if project.is_local()
&& project.language_servers_for_buffer(buffer, cx).count() == 0
{
None
} else {
Some(language)
}
})
.cloned()
.collect::<HashSet<_>>();
if !languages_affected.is_empty() {
self.refresh_inlays(
InlayRefreshReason::BufferEdited(languages_affected),
cx,
);
}
}
} }
multi_buffer::Event::ExcerptsAdded { multi_buffer::Event::ExcerptsAdded {
buffer, buffer,

View File

@ -15,6 +15,7 @@ pub struct EditorSettings {
pub struct Scrollbar { pub struct Scrollbar {
pub show: ShowScrollbar, pub show: ShowScrollbar,
pub git_diff: bool, pub git_diff: bool,
pub selections: bool,
} }
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@ -39,6 +40,7 @@ pub struct EditorSettingsContent {
pub struct ScrollbarContent { pub struct ScrollbarContent {
pub show: Option<ShowScrollbar>, pub show: Option<ShowScrollbar>,
pub git_diff: Option<bool>, pub git_diff: Option<bool>,
pub selections: Option<bool>,
} }
impl Setting for EditorSettings { impl Setting for EditorSettings {

View File

@ -6979,6 +6979,111 @@ async fn test_copilot_disabled_globs(
assert!(copilot_requests.try_next().is_ok()); assert!(copilot_requests.try_next().is_ok());
} }
#[gpui::test]
async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
brackets: BracketPairConfig {
pairs: vec![BracketPair {
start: "{".to_string(),
end: "}".to_string(),
close: true,
newline: true,
}],
disabled_scopes_by_bracket_ix: Vec::new(),
},
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
first_trigger_character: "{".to_string(),
more_trigger_character: None,
}),
..Default::default()
},
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/a",
json!({
"main.rs": "fn main() { let a = 5; }",
"other.rs": "// Test file",
}),
)
.await;
let project = Project::test(fs, ["/a".as_ref()], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let worktree_id = workspace.update(cx, |workspace, cx| {
workspace.project().read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
})
});
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/a/main.rs", cx)
})
.await
.unwrap();
cx.foreground().run_until_parked();
cx.foreground().start_waiting();
let fake_server = fake_servers.next().await.unwrap();
let editor_handle = workspace
.update(cx, |workspace, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
);
assert_eq!(
params.text_document_position.position,
lsp::Position::new(0, 21),
);
Ok(Some(vec![lsp::TextEdit {
new_text: "]".to_string(),
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
}]))
});
editor_handle.update(cx, |editor, cx| {
cx.focus(&editor_handle);
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
});
editor.handle_input("{", cx);
});
cx.foreground().run_until_parked();
buffer.read_with(cx, |buffer, _| {
assert_eq!(
buffer.text(),
"fn main() { let a = {5}; }",
"No extra braces from on type formatting should appear in the buffer"
)
});
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> { fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(row as u32, column as u32); let point = DisplayPoint::new(row as u32, column as u32);
point..point point..point

View File

@ -1008,6 +1008,7 @@ impl EditorElement {
bounds: RectF, bounds: RectF,
layout: &mut LayoutState, layout: &mut LayoutState,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
editor: &Editor,
) { ) {
enum ScrollbarMouseHandlers {} enum ScrollbarMouseHandlers {}
if layout.mode != EditorMode::Full { if layout.mode != EditorMode::Full {
@ -1050,9 +1051,74 @@ impl EditorElement {
background: style.track.background_color, background: style.track.background_color,
..Default::default() ..Default::default()
}); });
let scrollbar_settings = settings::get::<EditorSettings>(cx).scrollbar;
let theme = theme::current(cx);
let scrollbar_theme = &theme.editor.scrollbar;
if layout.is_singleton && scrollbar_settings.selections {
let start_anchor = Anchor::min();
let end_anchor = Anchor::max();
let mut start_row = None;
let mut end_row = None;
let color = scrollbar_theme.selections;
let border = Border {
width: 1.,
color: style.thumb.border.color,
overlay: false,
top: false,
right: true,
bottom: false,
left: true,
};
let mut push_region = |start, end| {
if let (Some(start_display), Some(end_display)) = (start, end) {
let start_y = y_for_row(start_display as f32);
let mut end_y = y_for_row(end_display as f32);
if end_y - start_y < 1. {
end_y = start_y + 1.;
}
let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y));
if layout.is_singleton && settings::get::<EditorSettings>(cx).scrollbar.git_diff { scene.push_quad(Quad {
let diff_style = theme::current(cx).editor.scrollbar.git.clone(); bounds,
background: Some(color),
border,
corner_radius: style.thumb.corner_radius,
})
}
};
for (row, _) in &editor.background_highlights_in_range(
start_anchor..end_anchor,
&layout.position_map.snapshot,
&theme,
) {
let start_display = row.start;
let end_display = row.end;
if start_row.is_none() {
assert_eq!(end_row, None);
start_row = Some(start_display.row());
end_row = Some(end_display.row());
continue;
}
if let Some(current_end) = end_row.as_mut() {
if start_display.row() > *current_end + 1 {
push_region(start_row, end_row);
start_row = Some(start_display.row());
end_row = Some(end_display.row());
} else {
// Merge two hunks.
*current_end = end_display.row();
}
} else {
unreachable!();
}
}
// We might still have a hunk that was not rendered (if there was a search hit on the last line)
push_region(start_row, end_row);
}
if layout.is_singleton && scrollbar_settings.git_diff {
let diff_style = scrollbar_theme.git.clone();
for hunk in layout for hunk in layout
.position_map .position_map
.snapshot .snapshot
@ -2368,7 +2434,7 @@ impl Element<Editor> for EditorElement {
if !layout.blocks.is_empty() { if !layout.blocks.is_empty() {
self.paint_blocks(scene, bounds, visible_bounds, layout, editor, cx); self.paint_blocks(scene, bounds, visible_bounds, layout, editor, cx);
} }
self.paint_scrollbar(scene, bounds, layout, cx); self.paint_scrollbar(scene, bounds, layout, cx, &editor);
scene.pop_layer(); scene.pop_layer();
scene.pop_layer(); scene.pop_layer();

View File

@ -38,14 +38,14 @@ pub struct CachedExcerptHints {
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum InvalidationStrategy { pub enum InvalidationStrategy {
RefreshRequested, RefreshRequested,
ExcerptEdited, BufferEdited,
None, None,
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct InlaySplice { pub struct InlaySplice {
pub to_remove: Vec<InlayId>, pub to_remove: Vec<InlayId>,
pub to_insert: Vec<(Anchor, InlayId, InlayHint)>, pub to_insert: Vec<Inlay>,
} }
struct UpdateTask { struct UpdateTask {
@ -94,7 +94,7 @@ impl InvalidationStrategy {
fn should_invalidate(&self) -> bool { fn should_invalidate(&self) -> bool {
matches!( matches!(
self, self,
InvalidationStrategy::RefreshRequested | InvalidationStrategy::ExcerptEdited InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited
) )
} }
} }
@ -197,7 +197,7 @@ impl InlayHintCache {
pub fn refresh_inlay_hints( pub fn refresh_inlay_hints(
&mut self, &mut self,
mut excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)>, mut excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
invalidate: InvalidationStrategy, invalidate: InvalidationStrategy,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) { ) {
@ -285,13 +285,13 @@ impl InlayHintCache {
if !old_kinds.contains(&cached_hint.kind) if !old_kinds.contains(&cached_hint.kind)
&& new_kinds.contains(&cached_hint.kind) && new_kinds.contains(&cached_hint.kind)
{ {
to_insert.push(( to_insert.push(Inlay::hint(
cached_hint_id.id(),
multi_buffer_snapshot.anchor_in_excerpt( multi_buffer_snapshot.anchor_in_excerpt(
*excerpt_id, *excerpt_id,
cached_hint.position, cached_hint.position,
), ),
*cached_hint_id, &cached_hint,
cached_hint.clone(),
)); ));
} }
excerpt_cache.next(); excerpt_cache.next();
@ -307,11 +307,11 @@ impl InlayHintCache {
for (cached_hint_id, maybe_missed_cached_hint) in excerpt_cache { for (cached_hint_id, maybe_missed_cached_hint) in excerpt_cache {
let cached_hint_kind = maybe_missed_cached_hint.kind; let cached_hint_kind = maybe_missed_cached_hint.kind;
if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) { if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
to_insert.push(( to_insert.push(Inlay::hint(
cached_hint_id.id(),
multi_buffer_snapshot multi_buffer_snapshot
.anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position), .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position),
*cached_hint_id, &maybe_missed_cached_hint,
maybe_missed_cached_hint.clone(),
)); ));
} }
} }
@ -342,30 +342,40 @@ impl InlayHintCache {
fn spawn_new_update_tasks( fn spawn_new_update_tasks(
editor: &mut Editor, editor: &mut Editor,
excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)>, excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
invalidate: InvalidationStrategy, invalidate: InvalidationStrategy,
update_cache_version: usize, update_cache_version: usize,
cx: &mut ViewContext<'_, '_, Editor>, cx: &mut ViewContext<'_, '_, Editor>,
) { ) {
let visible_hints = Arc::new(editor.visible_inlay_hints(cx)); let visible_hints = Arc::new(editor.visible_inlay_hints(cx));
for (excerpt_id, (buffer_handle, excerpt_visible_range)) in excerpts_to_query { for (excerpt_id, (buffer_handle, new_task_buffer_version, excerpt_visible_range)) in
if !excerpt_visible_range.is_empty() { excerpts_to_query
{
if excerpt_visible_range.is_empty() {
continue;
}
let buffer = buffer_handle.read(cx); let buffer = buffer_handle.read(cx);
let buffer_snapshot = buffer.snapshot(); let buffer_snapshot = buffer.snapshot();
if buffer_snapshot
.version()
.changed_since(&new_task_buffer_version)
{
continue;
}
let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned(); let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned();
if let Some(cached_excerpt_hints) = &cached_excerpt_hints { if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
let new_task_buffer_version = buffer_snapshot.version();
let cached_excerpt_hints = cached_excerpt_hints.read(); let cached_excerpt_hints = cached_excerpt_hints.read();
let cached_buffer_version = &cached_excerpt_hints.buffer_version; let cached_buffer_version = &cached_excerpt_hints.buffer_version;
if cached_excerpt_hints.version > update_cache_version if cached_excerpt_hints.version > update_cache_version
|| cached_buffer_version.changed_since(new_task_buffer_version) || cached_buffer_version.changed_since(&new_task_buffer_version)
{ {
return; continue;
} }
if !new_task_buffer_version.changed_since(&cached_buffer_version) if !new_task_buffer_version.changed_since(&cached_buffer_version)
&& !matches!(invalidate, InvalidationStrategy::RefreshRequested) && !matches!(invalidate, InvalidationStrategy::RefreshRequested)
{ {
return; continue;
} }
}; };
@ -417,7 +427,7 @@ fn spawn_new_update_tasks(
match (update_task.invalidate, invalidate) { match (update_task.invalidate, invalidate) {
(_, InvalidationStrategy::None) => {} (_, InvalidationStrategy::None) => {}
( (
InvalidationStrategy::ExcerptEdited, InvalidationStrategy::BufferEdited,
InvalidationStrategy::RefreshRequested, InvalidationStrategy::RefreshRequested,
) if !update_task.task.is_running_rx.is_closed() => { ) if !update_task.task.is_running_rx.is_closed() => {
update_task.pending_refresh = Some(query); update_task.pending_refresh = Some(query);
@ -443,7 +453,6 @@ fn spawn_new_update_tasks(
} }
} }
} }
}
} }
fn new_update_task( fn new_update_task(
@ -648,18 +657,22 @@ async fn fetch_and_update_hints(
for new_hint in new_update.add_to_cache { for new_hint in new_update.add_to_cache {
let new_hint_position = multi_buffer_snapshot let new_hint_position = multi_buffer_snapshot
.anchor_in_excerpt(query.excerpt_id, new_hint.position); .anchor_in_excerpt(query.excerpt_id, new_hint.position);
let new_inlay_id = InlayId::Hint(post_inc(&mut editor.next_inlay_id)); let new_inlay_id = post_inc(&mut editor.next_inlay_id);
if editor if editor
.inlay_hint_cache .inlay_hint_cache
.allowed_hint_kinds .allowed_hint_kinds
.contains(&new_hint.kind) .contains(&new_hint.kind)
{ {
splice splice.to_insert.push(Inlay::hint(
.to_insert new_inlay_id,
.push((new_hint_position, new_inlay_id, new_hint.clone())); new_hint_position,
&new_hint,
));
} }
cached_excerpt_hints.hints.push((new_inlay_id, new_hint)); cached_excerpt_hints
.hints
.push((InlayId::Hint(new_inlay_id), new_hint));
} }
cached_excerpt_hints cached_excerpt_hints
@ -820,7 +833,7 @@ mod tests {
use crate::{ use crate::{
scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount}, scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
serde_json::json, serde_json::json,
ExcerptRange, InlayHintSettings, ExcerptRange,
}; };
use futures::StreamExt; use futures::StreamExt;
use gpui::{executor::Deterministic, TestAppContext, ViewHandle}; use gpui::{executor::Deterministic, TestAppContext, ViewHandle};
@ -961,6 +974,348 @@ mod tests {
}); });
} }
#[gpui::test]
async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: true,
show_type_hints: true,
show_parameter_hints: true,
show_other_hints: true,
})
});
let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
let lsp_request_count = Arc::new(AtomicU32::new(0));
fake_server
.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
let task_lsp_request_count = Arc::clone(&lsp_request_count);
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(file_with_hints).unwrap(),
);
let current_call_id =
Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
Ok(Some(vec![lsp::InlayHint {
position: lsp::Position::new(0, current_call_id),
label: lsp::InlayHintLabel::String(current_call_id.to_string()),
kind: None,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
}]))
}
})
.next()
.await;
cx.foreground().run_until_parked();
let mut edits_made = 1;
editor.update(cx, |editor, cx| {
let expected_layers = vec!["0".to_string()];
assert_eq!(
expected_layers,
cached_hint_labels(editor),
"Should get its first hints when opening the editor"
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.version, edits_made,
"The editor update the cache version after every cache/view change"
);
});
let progress_token = "test_progress_token";
fake_server
.request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
token: lsp::ProgressToken::String(progress_token.to_string()),
})
.await
.expect("work done progress create request failed");
cx.foreground().run_until_parked();
fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
token: lsp::ProgressToken::String(progress_token.to_string()),
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
lsp::WorkDoneProgressBegin::default(),
)),
});
cx.foreground().run_until_parked();
editor.update(cx, |editor, cx| {
let expected_layers = vec!["0".to_string()];
assert_eq!(
expected_layers,
cached_hint_labels(editor),
"Should not update hints while the work task is running"
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.version, edits_made,
"Should not update the cache while the work task is running"
);
});
fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
token: lsp::ProgressToken::String(progress_token.to_string()),
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
lsp::WorkDoneProgressEnd::default(),
)),
});
cx.foreground().run_until_parked();
edits_made += 1;
editor.update(cx, |editor, cx| {
let expected_layers = vec!["1".to_string()];
assert_eq!(
expected_layers,
cached_hint_labels(editor),
"New hints should be queried after the work task is done"
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.version, edits_made,
"Cache version should udpate once after the work task is done"
);
});
}
#[gpui::test]
async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: true,
show_type_hints: true,
show_parameter_hints: true,
show_other_hints: true,
})
});
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/a",
json!({
"main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
"other.md": "Test md file with some text",
}),
)
.await;
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let worktree_id = workspace.update(cx, |workspace, cx| {
workspace.project().read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
})
});
let mut rs_fake_servers = None;
let mut md_fake_servers = None;
for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
let mut language = Language::new(
LanguageConfig {
name: name.into(),
path_suffixes: vec![path_suffix.to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
name,
capabilities: lsp::ServerCapabilities {
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
..Default::default()
},
..Default::default()
}))
.await;
match name {
"Rust" => rs_fake_servers = Some(fake_servers),
"Markdown" => md_fake_servers = Some(fake_servers),
_ => unreachable!(),
}
project.update(cx, |project, _| {
project.languages().add(Arc::new(language));
});
}
let _rs_buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/a/main.rs", cx)
})
.await
.unwrap();
cx.foreground().run_until_parked();
cx.foreground().start_waiting();
let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
let rs_editor = workspace
.update(cx, |workspace, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
rs_fake_server
.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
let task_lsp_request_count = Arc::clone(&rs_lsp_request_count);
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
);
let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
Ok(Some(vec![lsp::InlayHint {
position: lsp::Position::new(0, i),
label: lsp::InlayHintLabel::String(i.to_string()),
kind: None,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
}]))
}
})
.next()
.await;
cx.foreground().run_until_parked();
rs_editor.update(cx, |editor, cx| {
let expected_layers = vec!["0".to_string()];
assert_eq!(
expected_layers,
cached_hint_labels(editor),
"Should get its first hints when opening the editor"
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.version, 1,
"Rust editor update the cache version after every cache/view change"
);
});
cx.foreground().run_until_parked();
let _md_buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/a/other.md", cx)
})
.await
.unwrap();
cx.foreground().run_until_parked();
cx.foreground().start_waiting();
let md_fake_server = md_fake_servers.unwrap().next().await.unwrap();
let md_editor = workspace
.update(cx, |workspace, cx| {
workspace.open_path((worktree_id, "other.md"), None, true, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
let md_lsp_request_count = Arc::new(AtomicU32::new(0));
md_fake_server
.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
let task_lsp_request_count = Arc::clone(&md_lsp_request_count);
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/a/other.md").unwrap(),
);
let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
Ok(Some(vec![lsp::InlayHint {
position: lsp::Position::new(0, i),
label: lsp::InlayHintLabel::String(i.to_string()),
kind: None,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
}]))
}
})
.next()
.await;
cx.foreground().run_until_parked();
md_editor.update(cx, |editor, cx| {
let expected_layers = vec!["0".to_string()];
assert_eq!(
expected_layers,
cached_hint_labels(editor),
"Markdown editor should have a separate verison, repeating Rust editor rules"
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.version, 1);
});
rs_editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
editor.handle_input("some rs change", cx);
});
cx.foreground().run_until_parked();
rs_editor.update(cx, |editor, cx| {
let expected_layers = vec!["1".to_string()];
assert_eq!(
expected_layers,
cached_hint_labels(editor),
"Rust inlay cache should change after the edit"
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.version, 2,
"Every time hint cache changes, cache version should be incremented"
);
});
md_editor.update(cx, |editor, cx| {
let expected_layers = vec!["0".to_string()];
assert_eq!(
expected_layers,
cached_hint_labels(editor),
"Markdown editor should not be affected by Rust editor changes"
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.version, 1);
});
md_editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
editor.handle_input("some md change", cx);
});
cx.foreground().run_until_parked();
md_editor.update(cx, |editor, cx| {
let expected_layers = vec!["1".to_string()];
assert_eq!(
expected_layers,
cached_hint_labels(editor),
"Rust editor should not be affected by Markdown editor changes"
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.version, 2);
});
rs_editor.update(cx, |editor, cx| {
let expected_layers = vec!["1".to_string()];
assert_eq!(
expected_layers,
cached_hint_labels(editor),
"Markdown editor should also change independently"
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.version, 2);
});
}
#[gpui::test] #[gpui::test]
async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) { async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
@ -1079,7 +1434,6 @@ mod tests {
visible_hint_labels(editor, cx) visible_hint_labels(editor, cx)
); );
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
assert_eq!( assert_eq!(
inlay_cache.version, edits_made, inlay_cache.version, edits_made,
"Should not update cache version due to new loaded hints being the same" "Should not update cache version due to new loaded hints being the same"
@ -1215,7 +1569,6 @@ mod tests {
assert!(cached_hint_labels(editor).is_empty()); assert!(cached_hint_labels(editor).is_empty());
assert!(visible_hint_labels(editor, cx).is_empty()); assert!(visible_hint_labels(editor, cx).is_empty());
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds);
assert_eq!( assert_eq!(
inlay_cache.version, edits_made, inlay_cache.version, edits_made,
"The editor should not update the cache version after /refresh query without updates" "The editor should not update the cache version after /refresh query without updates"
@ -1289,20 +1642,18 @@ mod tests {
visible_hint_labels(editor, cx), visible_hint_labels(editor, cx),
); );
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds);
assert_eq!(inlay_cache.version, edits_made); assert_eq!(inlay_cache.version, edits_made);
}); });
} }
#[gpui::test] #[gpui::test]
async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) { async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
let allowed_hint_kinds = HashSet::from_iter([None]);
init_test(cx, |settings| { init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings { settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: true, enabled: true,
show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), show_type_hints: true,
show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), show_parameter_hints: true,
show_other_hints: allowed_hint_kinds.contains(&None), show_other_hints: true,
}) })
}); });
@ -1370,7 +1721,6 @@ mod tests {
); );
assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(expected_hints, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
assert_eq!( assert_eq!(
inlay_cache.version, 1, inlay_cache.version, 1,
"Only one update should be registered in the cache after all cancellations" "Only one update should be registered in the cache after all cancellations"
@ -1417,7 +1767,6 @@ mod tests {
); );
assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(expected_hints, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
assert_eq!( assert_eq!(
inlay_cache.version, 2, inlay_cache.version, 2,
"Should update the cache version once more, for the new change" "Should update the cache version once more, for the new change"
@ -1427,13 +1776,12 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) { async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
init_test(cx, |settings| { init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings { settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: true, enabled: true,
show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), show_type_hints: true,
show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), show_parameter_hints: true,
show_other_hints: allowed_hint_kinds.contains(&None), show_other_hints: true,
}) })
}); });
@ -1539,7 +1887,6 @@ mod tests {
); );
assert_eq!(expected_layers, visible_hint_labels(editor, cx)); assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
assert_eq!( assert_eq!(
inlay_cache.version, 2, inlay_cache.version, 2,
"Both LSP queries should've bumped the cache version" "Both LSP queries should've bumped the cache version"
@ -1572,7 +1919,6 @@ mod tests {
"Should have hints from the new LSP response after edit"); "Should have hints from the new LSP response after edit");
assert_eq!(expected_layers, visible_hint_labels(editor, cx)); assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
assert_eq!(inlay_cache.version, 5, "Should update the cache for every LSP response with hints added"); assert_eq!(inlay_cache.version, 5, "Should update the cache for every LSP response with hints added");
}); });
} }
@ -1582,13 +1928,12 @@ mod tests {
deterministic: Arc<Deterministic>, deterministic: Arc<Deterministic>,
cx: &mut gpui::TestAppContext, cx: &mut gpui::TestAppContext,
) { ) {
let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
init_test(cx, |settings| { init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings { settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: true, enabled: true,
show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), show_type_hints: true,
show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), show_parameter_hints: true,
show_other_hints: allowed_hint_kinds.contains(&None), show_other_hints: true,
}) })
}); });
@ -1794,7 +2139,6 @@ mod tests {
); );
assert_eq!(expected_layers, visible_hint_labels(editor, cx)); assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
assert_eq!(inlay_cache.version, 4, "Every visible excerpt hints should bump the verison"); assert_eq!(inlay_cache.version, 4, "Every visible excerpt hints should bump the verison");
}); });
@ -1826,7 +2170,6 @@ mod tests {
"With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
assert_eq!(expected_layers, visible_hint_labels(editor, cx)); assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
assert_eq!(inlay_cache.version, 9); assert_eq!(inlay_cache.version, 9);
}); });
@ -1855,7 +2198,6 @@ mod tests {
"After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
assert_eq!(expected_layers, visible_hint_labels(editor, cx)); assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
assert_eq!(inlay_cache.version, 12); assert_eq!(inlay_cache.version, 12);
}); });
@ -1884,7 +2226,6 @@ mod tests {
"After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
assert_eq!(expected_layers, visible_hint_labels(editor, cx)); assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
assert_eq!(inlay_cache.version, 12, "No updates should happen during scrolling already scolled buffer"); assert_eq!(inlay_cache.version, 12, "No updates should happen during scrolling already scolled buffer");
}); });
@ -1911,7 +2252,6 @@ mod tests {
unedited (2nd) buffer should have the same hint"); unedited (2nd) buffer should have the same hint");
assert_eq!(expected_layers, visible_hint_labels(editor, cx)); assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
assert_eq!(inlay_cache.version, 16); assert_eq!(inlay_cache.version, 16);
}); });
} }

View File

@ -193,7 +193,11 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
}) })
} }
pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint { pub fn start_of_paragraph(
map: &DisplaySnapshot,
display_point: DisplayPoint,
mut count: usize,
) -> DisplayPoint {
let point = display_point.to_point(map); let point = display_point.to_point(map);
if point.row == 0 { if point.row == 0 {
return map.max_point(); return map.max_point();
@ -203,8 +207,12 @@ pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) ->
for row in (0..point.row + 1).rev() { for row in (0..point.row + 1).rev() {
let blank = map.buffer_snapshot.is_line_blank(row); let blank = map.buffer_snapshot.is_line_blank(row);
if found_non_blank_line && blank { if found_non_blank_line && blank {
if count <= 1 {
return Point::new(row, 0).to_display_point(map); return Point::new(row, 0).to_display_point(map);
} }
count -= 1;
found_non_blank_line = false;
}
found_non_blank_line |= !blank; found_non_blank_line |= !blank;
} }
@ -212,7 +220,11 @@ pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) ->
DisplayPoint::zero() DisplayPoint::zero()
} }
pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint { pub fn end_of_paragraph(
map: &DisplaySnapshot,
display_point: DisplayPoint,
mut count: usize,
) -> DisplayPoint {
let point = display_point.to_point(map); let point = display_point.to_point(map);
if point.row == map.max_buffer_row() { if point.row == map.max_buffer_row() {
return DisplayPoint::zero(); return DisplayPoint::zero();
@ -222,8 +234,12 @@ pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> D
for row in point.row..map.max_buffer_row() + 1 { for row in point.row..map.max_buffer_row() + 1 {
let blank = map.buffer_snapshot.is_line_blank(row); let blank = map.buffer_snapshot.is_line_blank(row);
if found_non_blank_line && blank { if found_non_blank_line && blank {
if count <= 1 {
return Point::new(row, 0).to_display_point(map); return Point::new(row, 0).to_display_point(map);
} }
count -= 1;
found_non_blank_line = false;
}
found_non_blank_line |= !blank; found_non_blank_line |= !blank;
} }
@ -263,13 +279,13 @@ pub fn find_preceding_boundary(
if let Some((prev_ch, prev_point)) = prev { if let Some((prev_ch, prev_point)) = prev {
if is_boundary(ch, prev_ch) { if is_boundary(ch, prev_ch) {
return prev_point; return map.clip_point(prev_point, Bias::Left);
} }
} }
prev = Some((ch, point)); prev = Some((ch, point));
} }
DisplayPoint::zero() map.clip_point(DisplayPoint::zero(), Bias::Left)
} }
/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the /// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
@ -292,7 +308,7 @@ pub fn find_preceding_boundary_in_line(
for (ch, point) in map.reverse_chars_at(from) { for (ch, point) in map.reverse_chars_at(from) {
if let Some((prev_ch, prev_point)) = prev { if let Some((prev_ch, prev_point)) = prev {
if is_boundary(ch, prev_ch) { if is_boundary(ch, prev_ch) {
return prev_point; return map.clip_point(prev_point, Bias::Left);
} }
} }
@ -303,7 +319,7 @@ pub fn find_preceding_boundary_in_line(
prev = Some((ch, point)); prev = Some((ch, point));
} }
prev.map(|(_, point)| point).unwrap_or(from) map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Left)
} }
/// Scans for a boundary following the given start point until a boundary is found, indicated by the /// Scans for a boundary following the given start point until a boundary is found, indicated by the
@ -406,8 +422,12 @@ pub fn split_display_range_by_lines(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, MultiBuffer}; use crate::{
display_map::Inlay, test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange,
InlayId, MultiBuffer,
};
use settings::SettingsStore; use settings::SettingsStore;
use util::post_inc;
#[gpui::test] #[gpui::test]
fn test_previous_word_start(cx: &mut gpui::AppContext) { fn test_previous_word_start(cx: &mut gpui::AppContext) {
@ -505,6 +525,80 @@ mod tests {
}); });
} }
#[gpui::test]
fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::AppContext) {
init_test(cx);
let input_text = "abcdefghijklmnopqrstuvwxys";
let family_id = cx
.font_cache()
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let buffer = MultiBuffer::build_simple(input_text, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let display_map =
cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
// add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary
let mut id = 0;
let inlays = (0..buffer_snapshot.len())
.map(|offset| {
[
Inlay {
id: InlayId::Suggestion(post_inc(&mut id)),
position: buffer_snapshot.anchor_at(offset, Bias::Left),
text: format!("test").into(),
},
Inlay {
id: InlayId::Suggestion(post_inc(&mut id)),
position: buffer_snapshot.anchor_at(offset, Bias::Right),
text: format!("test").into(),
},
Inlay {
id: InlayId::Hint(post_inc(&mut id)),
position: buffer_snapshot.anchor_at(offset, Bias::Left),
text: format!("test").into(),
},
Inlay {
id: InlayId::Hint(post_inc(&mut id)),
position: buffer_snapshot.anchor_at(offset, Bias::Right),
text: format!("test").into(),
},
]
})
.flatten()
.collect();
let snapshot = display_map.update(cx, |map, cx| {
map.splice_inlays(Vec::new(), inlays, cx);
map.snapshot(cx)
});
assert_eq!(
find_preceding_boundary(
&snapshot,
buffer_snapshot.len().to_display_point(&snapshot),
|left, _| left == 'a',
),
0.to_display_point(&snapshot),
"Should not stop at inlays when looking for boundaries"
);
assert_eq!(
find_preceding_boundary_in_line(
&snapshot,
buffer_snapshot.len().to_display_point(&snapshot),
|left, _| left == 'a',
),
0.to_display_point(&snapshot),
"Should not stop at inlays when looking for boundaries in line"
);
}
#[gpui::test] #[gpui::test]
fn test_next_word_end(cx: &mut gpui::AppContext) { fn test_next_word_end(cx: &mut gpui::AppContext) {
init_test(cx); init_test(cx);

View File

@ -210,6 +210,10 @@ impl<'a> EditorTestContext<'a> {
self.assert_selections(expected_selections, marked_text.to_string()) 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)
}
#[track_caller] #[track_caller]
pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) { pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
let expected_ranges = self.ranges(marked_text); let expected_ranges = self.ranges(marked_text);
@ -248,14 +252,8 @@ impl<'a> EditorTestContext<'a> {
self.assert_selections(expected_selections, expected_marked_text) self.assert_selections(expected_selections, expected_marked_text)
} }
#[track_caller] fn editor_selections(&self) -> Vec<Range<usize>> {
fn assert_selections( self.editor
&mut self,
expected_selections: Vec<Range<usize>>,
expected_marked_text: String,
) {
let actual_selections = self
.editor
.read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx)) .read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
.into_iter() .into_iter()
.map(|s| { .map(|s| {
@ -265,12 +263,22 @@ impl<'a> EditorTestContext<'a> {
s.start..s.end s.start..s.end
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>()
}
#[track_caller]
fn assert_selections(
&mut self,
expected_selections: Vec<Range<usize>>,
expected_marked_text: String,
) {
let actual_selections = self.editor_selections();
let actual_marked_text = let actual_marked_text =
generate_marked_text(&self.buffer_text(), &actual_selections, true); generate_marked_text(&self.buffer_text(), &actual_selections, true);
if expected_selections != actual_selections { if expected_selections != actual_selections {
panic!( panic!(
indoc! {" indoc! {"
{}Editor has unexpected selections. {}Editor has unexpected selections.
Expected selections: Expected selections:

View File

@ -31,6 +31,7 @@ serde_derive.workspace = true
serde_json.workspace = true serde_json.workspace = true
log.workspace = true log.workspace = true
libc = "0.2" libc = "0.2"
time.workspace = true
[dev-dependencies] [dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }

View File

@ -279,6 +279,9 @@ impl Fs for RealFs {
async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> { async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
let buffer_size = text.summary().len.min(10 * 1024); let buffer_size = text.summary().len.min(10 * 1024);
if let Some(path) = path.parent() {
self.create_dir(path).await?;
}
let file = smol::fs::File::create(path).await?; let file = smol::fs::File::create(path).await?;
let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file); let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
for chunk in chunks(text, line_ending) { for chunk in chunks(text, line_ending) {
@ -1077,6 +1080,9 @@ impl Fs for FakeFs {
self.simulate_random_delay().await; self.simulate_random_delay().await;
let path = normalize_path(path); let path = normalize_path(path);
let content = chunks(text, line_ending).collect(); let content = chunks(text, line_ending).collect();
if let Some(path) = path.parent() {
self.create_dir(path).await?;
}
self.write_file_internal(path, content)?; self.write_file_internal(path, content)?;
Ok(()) Ok(())
} }

View File

@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use collections::HashMap; use collections::HashMap;
use git2::ErrorCode; use git2::{BranchType, ErrorCode};
use parking_lot::Mutex; use parking_lot::Mutex;
use rpc::proto; use rpc::proto;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
@ -16,6 +16,12 @@ use util::ResultExt;
pub use git2::Repository as LibGitRepository; pub use git2::Repository as LibGitRepository;
#[derive(Clone, Debug, Hash, PartialEq)]
pub struct Branch {
pub name: Box<str>,
/// Timestamp of most recent commit, normalized to Unix Epoch format.
pub unix_timestamp: Option<i64>,
}
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait GitRepository: Send { pub trait GitRepository: Send {
fn reload_index(&self); fn reload_index(&self);
@ -27,6 +33,12 @@ pub trait GitRepository: Send {
fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>>; fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>>;
fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>>; fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>>;
fn branches(&self) -> Result<Vec<Branch>> {
Ok(vec![])
}
fn change_branch(&self, _: &str) -> Result<()> {
Ok(())
}
} }
impl std::fmt::Debug for dyn GitRepository { impl std::fmt::Debug for dyn GitRepository {
@ -106,6 +118,40 @@ impl GitRepository for LibGitRepository {
} }
} }
} }
fn branches(&self) -> Result<Vec<Branch>> {
let local_branches = self.branches(Some(BranchType::Local))?;
let valid_branches = local_branches
.filter_map(|branch| {
branch.ok().and_then(|(branch, _)| {
let name = branch.name().ok().flatten().map(Box::from)?;
let timestamp = branch.get().peel_to_commit().ok()?.time();
let unix_timestamp = timestamp.seconds();
let timezone_offset = timestamp.offset_minutes();
let utc_offset =
time::UtcOffset::from_whole_seconds(timezone_offset * 60).ok()?;
let unix_timestamp =
time::OffsetDateTime::from_unix_timestamp(unix_timestamp).ok()?;
Some(Branch {
name,
unix_timestamp: Some(unix_timestamp.to_offset(utc_offset).unix_timestamp()),
})
})
})
.collect();
Ok(valid_branches)
}
fn change_branch(&self, name: &str) -> Result<()> {
let revision = self.find_branch(name, BranchType::Local)?;
let revision = revision.get();
let as_tree = revision.peel_to_tree()?;
self.checkout_tree(as_tree.as_object(), None)?;
self.set_head(
revision
.name()
.ok_or_else(|| anyhow::anyhow!("Branch name could not be retrieved"))?,
)?;
Ok(())
}
} }
fn read_status(status: git2::Status) -> Option<GitFileStatus> { fn read_status(status: git2::Status) -> Option<GitFileStatus> {

View File

@ -24,6 +24,7 @@ pub struct GoToLine {
prev_scroll_position: Option<Vector2F>, prev_scroll_position: Option<Vector2F>,
cursor_point: Point, cursor_point: Point,
max_point: Point, max_point: Point,
has_focus: bool,
} }
pub enum Event { pub enum Event {
@ -57,6 +58,7 @@ impl GoToLine {
prev_scroll_position: scroll_position, prev_scroll_position: scroll_position,
cursor_point, cursor_point,
max_point, max_point,
has_focus: false,
} }
} }
@ -178,11 +180,20 @@ impl View for GoToLine {
} }
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) { fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
self.has_focus = true;
cx.focus(&self.line_editor); cx.focus(&self.line_editor);
} }
fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
self.has_focus = false;
}
} }
impl Modal for GoToLine { impl Modal for GoToLine {
fn has_focus(&self) -> bool {
self.has_focus
}
fn dismiss_on_event(event: &Self::Event) -> bool { fn dismiss_on_event(event: &Self::Event) -> bool {
matches!(event, Event::Dismissed) matches!(event, Event::Dismissed)
} }

View File

@ -2971,14 +2971,12 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
} }
pub fn focus(&mut self, handle: &AnyViewHandle) { pub fn focus(&mut self, handle: &AnyViewHandle) {
self.window_context self.window_context.focus(Some(handle.view_id));
.focus(handle.window_id, Some(handle.view_id));
} }
pub fn focus_self(&mut self) { pub fn focus_self(&mut self) {
let window_id = self.window_id;
let view_id = self.view_id; let view_id = self.view_id;
self.window_context.focus(window_id, Some(view_id)); self.window_context.focus(Some(view_id));
} }
pub fn is_self_focused(&self) -> bool { pub fn is_self_focused(&self) -> bool {
@ -2997,8 +2995,7 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
} }
pub fn blur(&mut self) { pub fn blur(&mut self) {
let window_id = self.window_id; self.window_context.focus(None);
self.window_context.focus(window_id, None);
} }
pub fn on_window_should_close<F>(&mut self, mut callback: F) pub fn on_window_should_close<F>(&mut self, mut callback: F)
@ -3304,11 +3301,15 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
let region_id = MouseRegionId::new::<Tag>(self.view_id, region_id); let region_id = MouseRegionId::new::<Tag>(self.view_id, region_id);
MouseState { MouseState {
hovered: self.window.hovered_region_ids.contains(&region_id), hovered: self.window.hovered_region_ids.contains(&region_id),
clicked: self clicked: if let Some((clicked_region_id, button)) = self.window.clicked_region {
.window if region_id == clicked_region_id {
.clicked_region_ids Some(button)
.get(&region_id) } else {
.and_then(|_| self.window.clicked_button), None
}
} else {
None
},
accessed_hovered: false, accessed_hovered: false,
accessed_clicked: false, accessed_clicked: false,
} }

View File

@ -8,14 +8,14 @@ use crate::{
MouseButton, MouseMovedEvent, PromptLevel, WindowBounds, MouseButton, MouseMovedEvent, PromptLevel, WindowBounds,
}, },
scene::{ scene::{
CursorRegion, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover, CursorRegion, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag, MouseEvent,
MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, Scene, MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, Scene,
}, },
text_layout::TextLayoutCache, text_layout::TextLayoutCache,
util::post_inc, util::post_inc,
Action, AnyView, AnyViewHandle, AppContext, BorrowAppContext, BorrowWindowContext, Effect, Action, AnyView, AnyViewHandle, AppContext, BorrowAppContext, BorrowWindowContext, Effect,
Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, SceneBuilder, Subscription, Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, NoAction, SceneBuilder,
View, ViewContext, ViewHandle, WindowInvalidation, Subscription, View, ViewContext, ViewHandle, WindowInvalidation,
}; };
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
@ -53,7 +53,7 @@ pub struct Window {
last_mouse_moved_event: Option<Event>, last_mouse_moved_event: Option<Event>,
pub(crate) hovered_region_ids: HashSet<MouseRegionId>, pub(crate) hovered_region_ids: HashSet<MouseRegionId>,
pub(crate) clicked_region_ids: HashSet<MouseRegionId>, pub(crate) clicked_region_ids: HashSet<MouseRegionId>,
pub(crate) clicked_button: Option<MouseButton>, pub(crate) clicked_region: Option<(MouseRegionId, MouseButton)>,
mouse_position: Vector2F, mouse_position: Vector2F,
text_layout_cache: TextLayoutCache, text_layout_cache: TextLayoutCache,
} }
@ -86,7 +86,7 @@ impl Window {
last_mouse_moved_event: None, last_mouse_moved_event: None,
hovered_region_ids: Default::default(), hovered_region_ids: Default::default(),
clicked_region_ids: Default::default(), clicked_region_ids: Default::default(),
clicked_button: None, clicked_region: None,
mouse_position: vec2f(0., 0.), mouse_position: vec2f(0., 0.),
titlebar_height, titlebar_height,
appearance, appearance,
@ -434,7 +434,11 @@ impl<'a> WindowContext<'a> {
MatchResult::None => false, MatchResult::None => false,
MatchResult::Pending => true, MatchResult::Pending => true,
MatchResult::Matches(matches) => { MatchResult::Matches(matches) => {
let no_action_id = (NoAction {}).id();
for (view_id, action) in matches { for (view_id, action) in matches {
if action.id() == no_action_id {
return false;
}
if self.dispatch_action(Some(*view_id), action.as_ref()) { if self.dispatch_action(Some(*view_id), action.as_ref()) {
self.keystroke_matcher.clear_pending(); self.keystroke_matcher.clear_pending();
handled_by = Some(action.boxed_clone()); handled_by = Some(action.boxed_clone());
@ -480,8 +484,8 @@ impl<'a> WindowContext<'a> {
// specific ancestor element that contained both [positions]' // specific ancestor element that contained both [positions]'
// So we need to store the overlapping regions on mouse down. // So we need to store the overlapping regions on mouse down.
// If there is already clicked_button stored, don't replace it. // If there is already region being clicked, don't replace it.
if self.window.clicked_button.is_none() { if self.window.clicked_region.is_none() {
self.window.clicked_region_ids = self self.window.clicked_region_ids = self
.window .window
.mouse_regions .mouse_regions
@ -495,7 +499,17 @@ impl<'a> WindowContext<'a> {
}) })
.collect(); .collect();
self.window.clicked_button = Some(e.button); let mut highest_z_index = 0;
let mut clicked_region_id = None;
for (region, z_index) in self.window.mouse_regions.iter() {
if region.bounds.contains_point(e.position) && *z_index >= highest_z_index {
highest_z_index = *z_index;
clicked_region_id = Some(region.id());
}
}
self.window.clicked_region =
clicked_region_id.map(|region_id| (region_id, e.button));
} }
mouse_events.push(MouseEvent::Down(MouseDown { mouse_events.push(MouseEvent::Down(MouseDown {
@ -524,6 +538,10 @@ impl<'a> WindowContext<'a> {
region: Default::default(), region: Default::default(),
platform_event: e.clone(), platform_event: e.clone(),
})); }));
mouse_events.push(MouseEvent::ClickOut(MouseClickOut {
region: Default::default(),
platform_event: e.clone(),
}));
} }
Event::MouseMoved( Event::MouseMoved(
@ -556,7 +574,7 @@ impl<'a> WindowContext<'a> {
prev_mouse_position: self.window.mouse_position, prev_mouse_position: self.window.mouse_position,
platform_event: e.clone(), platform_event: e.clone(),
})); }));
} else if let Some(clicked_button) = self.window.clicked_button { } else if let Some((_, clicked_button)) = self.window.clicked_region {
// Mouse up event happened outside the current window. Simulate mouse up button event // Mouse up event happened outside the current window. Simulate mouse up button event
let button_event = e.to_button_event(clicked_button); let button_event = e.to_button_event(clicked_button);
mouse_events.push(MouseEvent::Up(MouseUp { mouse_events.push(MouseEvent::Up(MouseUp {
@ -679,8 +697,8 @@ impl<'a> WindowContext<'a> {
// Only raise click events if the released button is the same as the one stored // Only raise click events if the released button is the same as the one stored
if self if self
.window .window
.clicked_button .clicked_region
.map(|clicked_button| clicked_button == e.button) .map(|(_, clicked_button)| clicked_button == e.button)
.unwrap_or(false) .unwrap_or(false)
{ {
// Clear clicked regions and clicked button // Clear clicked regions and clicked button
@ -688,7 +706,7 @@ impl<'a> WindowContext<'a> {
&mut self.window.clicked_region_ids, &mut self.window.clicked_region_ids,
Default::default(), Default::default(),
); );
self.window.clicked_button = None; self.window.clicked_region = None;
// Find regions which still overlap with the mouse since the last MouseDown happened // Find regions which still overlap with the mouse since the last MouseDown happened
for (mouse_region, _) in self.window.mouse_regions.iter().rev() { for (mouse_region, _) in self.window.mouse_regions.iter().rev() {
@ -712,7 +730,10 @@ impl<'a> WindowContext<'a> {
} }
} }
MouseEvent::MoveOut(_) | MouseEvent::UpOut(_) | MouseEvent::DownOut(_) => { MouseEvent::MoveOut(_)
| MouseEvent::UpOut(_)
| MouseEvent::DownOut(_)
| MouseEvent::ClickOut(_) => {
for (mouse_region, _) in self.window.mouse_regions.iter().rev() { for (mouse_region, _) in self.window.mouse_regions.iter().rev() {
// NOT contains // NOT contains
if !mouse_region if !mouse_region
@ -860,18 +881,10 @@ impl<'a> WindowContext<'a> {
} }
for view_id in &invalidation.updated { for view_id in &invalidation.updated {
let titlebar_height = self.window.titlebar_height; let titlebar_height = self.window.titlebar_height;
let hovered_region_ids = self.window.hovered_region_ids.clone();
let clicked_region_ids = self
.window
.clicked_button
.map(|button| (self.window.clicked_region_ids.clone(), button));
let element = self let element = self
.render_view(RenderParams { .render_view(RenderParams {
view_id: *view_id, view_id: *view_id,
titlebar_height, titlebar_height,
hovered_region_ids,
clicked_region_ids,
refreshing: false, refreshing: false,
appearance, appearance,
}) })
@ -1085,6 +1098,10 @@ impl<'a> WindowContext<'a> {
self.window.focused_view_id self.window.focused_view_id
} }
pub fn focus(&mut self, view_id: Option<usize>) {
self.app_context.focus(self.window_id, view_id);
}
pub fn window_bounds(&self) -> WindowBounds { pub fn window_bounds(&self) -> WindowBounds {
self.window.platform_window.bounds() self.window.platform_window.bounds()
} }
@ -1176,8 +1193,6 @@ impl<'a> WindowContext<'a> {
pub struct RenderParams { pub struct RenderParams {
pub view_id: usize, pub view_id: usize,
pub titlebar_height: f32, pub titlebar_height: f32,
pub hovered_region_ids: HashSet<MouseRegionId>,
pub clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
pub refreshing: bool, pub refreshing: bool,
pub appearance: Appearance, pub appearance: Appearance,
} }

View File

@ -7,8 +7,8 @@ use crate::{
platform::CursorStyle, platform::CursorStyle,
platform::MouseButton, platform::MouseButton,
scene::{ scene::{
CursorRegion, HandlerSet, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseHover, CursorRegion, HandlerSet, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag,
MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
}, },
AnyElement, Element, EventContext, LayoutContext, MouseRegion, MouseState, SceneBuilder, AnyElement, Element, EventContext, LayoutContext, MouseRegion, MouseState, SceneBuilder,
SizeConstraint, View, ViewContext, SizeConstraint, View, ViewContext,
@ -136,6 +136,15 @@ impl<Tag, V: View> MouseEventHandler<Tag, V> {
self self
} }
pub fn on_click_out(
mut self,
button: MouseButton,
handler: impl Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
) -> Self {
self.handlers = self.handlers.on_click_out(button, handler);
self
}
pub fn on_down_out( pub fn on_down_out(
mut self, mut self,
button: MouseButton, button: MouseButton,

View File

@ -31,3 +31,5 @@ pub use window::{Axis, SizeConstraint, Vector2FExt, WindowContext};
pub use anyhow; pub use anyhow;
pub use serde_json; pub use serde_json;
actions!(zed, [NoAction]);

View File

@ -4,7 +4,7 @@ use pathfinder_geometry::vector::vec2f;
use crate::{geometry::vector::Vector2F, keymap_matcher::Keystroke}; use crate::{geometry::vector::Vector2F, keymap_matcher::Keystroke};
#[derive(Clone, Debug)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct KeyDownEvent { pub struct KeyDownEvent {
pub keystroke: Keystroke, pub keystroke: Keystroke,
pub is_held: bool, pub is_held: bool,

View File

@ -232,10 +232,6 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
sel!(canBecomeKeyWindow), sel!(canBecomeKeyWindow),
yes as extern "C" fn(&Object, Sel) -> BOOL, yes as extern "C" fn(&Object, Sel) -> BOOL,
); );
decl.add_method(
sel!(sendEvent:),
send_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method( decl.add_method(
sel!(windowDidResize:), sel!(windowDidResize:),
window_did_resize as extern "C" fn(&Object, Sel, id), window_did_resize as extern "C" fn(&Object, Sel, id),
@ -299,7 +295,7 @@ struct WindowState {
appearance_changed_callback: Option<Box<dyn FnMut()>>, appearance_changed_callback: Option<Box<dyn FnMut()>>,
input_handler: Option<Box<dyn InputHandler>>, input_handler: Option<Box<dyn InputHandler>>,
pending_key_down: Option<(KeyDownEvent, Option<InsertText>)>, pending_key_down: Option<(KeyDownEvent, Option<InsertText>)>,
performed_key_equivalent: bool, last_key_equivalent: Option<KeyDownEvent>,
synthetic_drag_counter: usize, synthetic_drag_counter: usize,
executor: Rc<executor::Foreground>, executor: Rc<executor::Foreground>,
scene_to_render: Option<Scene>, scene_to_render: Option<Scene>,
@ -521,7 +517,7 @@ impl Window {
appearance_changed_callback: None, appearance_changed_callback: None,
input_handler: None, input_handler: None,
pending_key_down: None, pending_key_down: None,
performed_key_equivalent: false, last_key_equivalent: None,
synthetic_drag_counter: 0, synthetic_drag_counter: 0,
executor, executor,
scene_to_render: Default::default(), scene_to_render: Default::default(),
@ -965,17 +961,19 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
let window_height = window_state_borrow.content_size().y(); let window_height = window_state_borrow.content_size().y();
let event = unsafe { Event::from_native(native_event, Some(window_height)) }; let event = unsafe { Event::from_native(native_event, Some(window_height)) };
if let Some(event) = event { if let Some(Event::KeyDown(event)) = event {
// For certain keystrokes, macOS will first dispatch a "key equivalent" event.
// If that event isn't handled, it will then dispatch a "key down" event. GPUI
// makes no distinction between these two types of events, so we need to ignore
// the "key down" event if we've already just processed its "key equivalent" version.
if key_equivalent { if key_equivalent {
window_state_borrow.performed_key_equivalent = true; window_state_borrow.last_key_equivalent = Some(event.clone());
} else if window_state_borrow.performed_key_equivalent { } else if window_state_borrow.last_key_equivalent.take().as_ref() == Some(&event) {
return NO; return NO;
} }
let function_is_held;
window_state_borrow.pending_key_down = match event {
Event::KeyDown(event) => {
let keydown = event.keystroke.clone(); let keydown = event.keystroke.clone();
let fn_modifier = keydown.function;
// Ignore events from held-down keys after some of the initially-pressed keys // Ignore events from held-down keys after some of the initially-pressed keys
// were released. // were released.
if event.is_held { if event.is_held {
@ -985,16 +983,12 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
} else { } else {
window_state_borrow.last_fresh_keydown = Some(keydown); window_state_borrow.last_fresh_keydown = Some(keydown);
} }
function_is_held = event.keystroke.function; window_state_borrow.pending_key_down = Some((event, None));
Some((event, None))
}
_ => return NO,
};
drop(window_state_borrow); drop(window_state_borrow);
if !function_is_held { // Send the event to the input context for IME handling, unless the `fn` modifier is
// being pressed.
if !fn_modifier {
unsafe { unsafe {
let input_context: id = msg_send![this, inputContext]; let input_context: id = msg_send![this, inputContext];
let _: BOOL = msg_send![input_context, handleEvent: native_event]; let _: BOOL = msg_send![input_context, handleEvent: native_event];
@ -1143,13 +1137,6 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
} }
} }
extern "C" fn send_event(this: &Object, _: Sel, native_event: id) {
unsafe {
let _: () = msg_send![super(this, class!(NSWindow)), sendEvent: native_event];
get_window_state(this).borrow_mut().performed_key_equivalent = false;
}
}
extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) { extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) }; let window_state = unsafe { get_window_state(this) };
window_state.as_ref().borrow().move_traffic_light(); window_state.as_ref().borrow().move_traffic_light();

View File

@ -99,6 +99,20 @@ impl Deref for MouseClick {
} }
} }
#[derive(Debug, Default, Clone)]
pub struct MouseClickOut {
pub region: RectF,
pub platform_event: MouseButtonEvent,
}
impl Deref for MouseClickOut {
type Target = MouseButtonEvent;
fn deref(&self) -> &Self::Target {
&self.platform_event
}
}
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct MouseDownOut { pub struct MouseDownOut {
pub region: RectF, pub region: RectF,
@ -150,6 +164,7 @@ pub enum MouseEvent {
Down(MouseDown), Down(MouseDown),
Up(MouseUp), Up(MouseUp),
Click(MouseClick), Click(MouseClick),
ClickOut(MouseClickOut),
DownOut(MouseDownOut), DownOut(MouseDownOut),
UpOut(MouseUpOut), UpOut(MouseUpOut),
ScrollWheel(MouseScrollWheel), ScrollWheel(MouseScrollWheel),
@ -165,6 +180,7 @@ impl MouseEvent {
MouseEvent::Down(r) => r.region = region, MouseEvent::Down(r) => r.region = region,
MouseEvent::Up(r) => r.region = region, MouseEvent::Up(r) => r.region = region,
MouseEvent::Click(r) => r.region = region, MouseEvent::Click(r) => r.region = region,
MouseEvent::ClickOut(r) => r.region = region,
MouseEvent::DownOut(r) => r.region = region, MouseEvent::DownOut(r) => r.region = region,
MouseEvent::UpOut(r) => r.region = region, MouseEvent::UpOut(r) => r.region = region,
MouseEvent::ScrollWheel(r) => r.region = region, MouseEvent::ScrollWheel(r) => r.region = region,
@ -182,6 +198,7 @@ impl MouseEvent {
MouseEvent::Down(_) => true, MouseEvent::Down(_) => true,
MouseEvent::Up(_) => true, MouseEvent::Up(_) => true,
MouseEvent::Click(_) => true, MouseEvent::Click(_) => true,
MouseEvent::ClickOut(_) => true,
MouseEvent::DownOut(_) => false, MouseEvent::DownOut(_) => false,
MouseEvent::UpOut(_) => false, MouseEvent::UpOut(_) => false,
MouseEvent::ScrollWheel(_) => true, MouseEvent::ScrollWheel(_) => true,
@ -222,6 +239,10 @@ impl MouseEvent {
discriminant(&MouseEvent::Click(Default::default())) discriminant(&MouseEvent::Click(Default::default()))
} }
pub fn click_out_disc() -> Discriminant<MouseEvent> {
discriminant(&MouseEvent::ClickOut(Default::default()))
}
pub fn down_out_disc() -> Discriminant<MouseEvent> { pub fn down_out_disc() -> Discriminant<MouseEvent> {
discriminant(&MouseEvent::DownOut(Default::default())) discriminant(&MouseEvent::DownOut(Default::default()))
} }
@ -239,6 +260,7 @@ impl MouseEvent {
MouseEvent::Down(e) => HandlerKey::new(Self::down_disc(), Some(e.button)), MouseEvent::Down(e) => HandlerKey::new(Self::down_disc(), Some(e.button)),
MouseEvent::Up(e) => HandlerKey::new(Self::up_disc(), Some(e.button)), MouseEvent::Up(e) => HandlerKey::new(Self::up_disc(), Some(e.button)),
MouseEvent::Click(e) => HandlerKey::new(Self::click_disc(), Some(e.button)), MouseEvent::Click(e) => HandlerKey::new(Self::click_disc(), Some(e.button)),
MouseEvent::ClickOut(e) => HandlerKey::new(Self::click_out_disc(), Some(e.button)),
MouseEvent::UpOut(e) => HandlerKey::new(Self::up_out_disc(), Some(e.button)), MouseEvent::UpOut(e) => HandlerKey::new(Self::up_out_disc(), Some(e.button)),
MouseEvent::DownOut(e) => HandlerKey::new(Self::down_out_disc(), Some(e.button)), MouseEvent::DownOut(e) => HandlerKey::new(Self::down_out_disc(), Some(e.button)),
MouseEvent::ScrollWheel(_) => HandlerKey::new(Self::scroll_wheel_disc(), None), MouseEvent::ScrollWheel(_) => HandlerKey::new(Self::scroll_wheel_disc(), None),

View File

@ -14,7 +14,7 @@ use super::{
MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover, MouseMove, MouseUp, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover, MouseMove, MouseUp,
MouseUpOut, MouseUpOut,
}, },
MouseMoveOut, MouseScrollWheel, MouseClickOut, MouseMoveOut, MouseScrollWheel,
}; };
#[derive(Clone)] #[derive(Clone)]
@ -89,6 +89,15 @@ impl MouseRegion {
self self
} }
pub fn on_click_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
V: View,
F: Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_click_out(button, handler);
self
}
pub fn on_down_out<V, F>(mut self, button: MouseButton, handler: F) -> Self pub fn on_down_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
where where
V: View, V: View,
@ -246,6 +255,10 @@ impl HandlerSet {
HandlerKey::new(MouseEvent::click_disc(), Some(button)), HandlerKey::new(MouseEvent::click_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]), SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
); );
set.insert(
HandlerKey::new(MouseEvent::click_out_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
);
set.insert( set.insert(
HandlerKey::new(MouseEvent::down_out_disc(), Some(button)), HandlerKey::new(MouseEvent::down_out_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]), SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
@ -405,6 +418,28 @@ impl HandlerSet {
self self
} }
pub fn on_click_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
V: View,
F: Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::click_out_disc(), Some(button),
Rc::new(move |region_event, view, cx, view_id| {
if let MouseEvent::ClickOut(e) = region_event {
let view = view.downcast_mut().unwrap();
let mut cx = ViewContext::mutable(cx, view_id);
let mut cx = EventContext::new(&mut cx);
handler(e, view, &mut cx);
cx.handled
} else {
panic!(
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::ClickOut, found {:?}",
region_event);
}
}));
self
}
pub fn on_down_out<V, F>(mut self, button: MouseButton, handler: F) -> Self pub fn on_down_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
where where
V: View, V: View,

View File

@ -4,7 +4,6 @@ mod syntax_map_tests;
use crate::{Grammar, InjectionConfig, Language, LanguageRegistry}; use crate::{Grammar, InjectionConfig, Language, LanguageRegistry};
use collections::HashMap; use collections::HashMap;
use futures::FutureExt; use futures::FutureExt;
use lazy_static::lazy_static;
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{ use std::{
borrow::Cow, borrow::Cow,
@ -25,9 +24,7 @@ thread_local! {
static PARSER: RefCell<Parser> = RefCell::new(Parser::new()); static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
} }
lazy_static! { static QUERY_CURSORS: Mutex<Vec<QueryCursor>> = Mutex::new(vec![]);
static ref QUERY_CURSORS: Mutex<Vec<QueryCursor>> = Default::default();
}
#[derive(Default)] #[derive(Default)]
pub struct SyntaxMap { pub struct SyntaxMap {

View File

@ -17,7 +17,6 @@ test-support = [
"async-trait", "async-trait",
"collections/test-support", "collections/test-support",
"gpui/test-support", "gpui/test-support",
"lazy_static",
"live_kit_server", "live_kit_server",
"nanoid", "nanoid",
] ]
@ -38,7 +37,6 @@ parking_lot.workspace = true
postage.workspace = true postage.workspace = true
async-trait = { workspace = true, optional = true } async-trait = { workspace = true, optional = true }
lazy_static = { workspace = true, optional = true }
nanoid = { version ="0.4", optional = true} nanoid = { version ="0.4", optional = true}
[dev-dependencies] [dev-dependencies]
@ -60,7 +58,6 @@ foreign-types = "0.3"
futures.workspace = true futures.workspace = true
hmac = "0.12" hmac = "0.12"
jwt = "0.16" jwt = "0.16"
lazy_static.workspace = true
objc = "0.2" objc = "0.2"
parking_lot.workspace = true parking_lot.workspace = true
serde.workspace = true serde.workspace = true

View File

@ -1,18 +1,15 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use async_trait::async_trait; use async_trait::async_trait;
use collections::HashMap; use collections::{BTreeMap, HashMap};
use futures::Stream; use futures::Stream;
use gpui::executor::Background; use gpui::executor::Background;
use lazy_static::lazy_static;
use live_kit_server::token; use live_kit_server::token;
use media::core_video::CVImageBuffer; use media::core_video::CVImageBuffer;
use parking_lot::Mutex; use parking_lot::Mutex;
use postage::watch; use postage::watch;
use std::{future::Future, mem, sync::Arc}; use std::{future::Future, mem, sync::Arc};
lazy_static! { static SERVERS: Mutex<BTreeMap<String, Arc<TestServer>>> = Mutex::new(BTreeMap::new());
static ref SERVERS: Mutex<HashMap<String, Arc<TestServer>>> = Default::default();
}
pub struct TestServer { pub struct TestServer {
pub url: String, pub url: String,

View File

@ -25,6 +25,7 @@ pub struct Picker<D: PickerDelegate> {
theme: Arc<Mutex<Box<dyn Fn(&theme::Theme) -> theme::Picker>>>, theme: Arc<Mutex<Box<dyn Fn(&theme::Theme) -> theme::Picker>>>,
confirmed: bool, confirmed: bool,
pending_update_matches: Task<Option<()>>, pending_update_matches: Task<Option<()>>,
has_focus: bool,
} }
pub trait PickerDelegate: Sized + 'static { pub trait PickerDelegate: Sized + 'static {
@ -45,10 +46,16 @@ pub trait PickerDelegate: Sized + 'static {
fn center_selection_after_match_updates(&self) -> bool { fn center_selection_after_match_updates(&self) -> bool {
false false
} }
fn render_header(&self, _cx: &AppContext) -> Option<AnyElement<Picker<Self>>> { fn render_header(
&self,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<AnyElement<Picker<Self>>> {
None None
} }
fn render_footer(&self, _cx: &AppContext) -> Option<AnyElement<Picker<Self>>> { fn render_footer(
&self,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<AnyElement<Picker<Self>>> {
None None
} }
} }
@ -140,13 +147,22 @@ impl<D: PickerDelegate> View for Picker<D> {
} }
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) { fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
self.has_focus = true;
if cx.is_self_focused() { if cx.is_self_focused() {
cx.focus(&self.query_editor); cx.focus(&self.query_editor);
} }
} }
fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
self.has_focus = false;
}
} }
impl<D: PickerDelegate> Modal for Picker<D> { impl<D: PickerDelegate> Modal for Picker<D> {
fn has_focus(&self) -> bool {
self.has_focus
}
fn dismiss_on_event(event: &Self::Event) -> bool { fn dismiss_on_event(event: &Self::Event) -> bool {
matches!(event, PickerEvent::Dismiss) matches!(event, PickerEvent::Dismiss)
} }
@ -191,6 +207,7 @@ impl<D: PickerDelegate> Picker<D> {
theme, theme,
confirmed: false, confirmed: false,
pending_update_matches: Task::ready(None), pending_update_matches: Task::ready(None),
has_focus: false,
}; };
this.update_matches(String::new(), cx); this.update_matches(String::new(), cx);
this this

View File

@ -64,7 +64,7 @@ itertools = "0.10"
[dev-dependencies] [dev-dependencies]
ctor.workspace = true ctor.workspace = true
env_logger.workspace = true env_logger.workspace = true
pretty_assertions = "1.3.0" pretty_assertions.workspace = true
client = { path = "../client", features = ["test-support"] } client = { path = "../client", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] } collections = { path = "../collections", features = ["test-support"] }
db = { path = "../db", features = ["test-support"] } db = { path = "../db", features = ["test-support"] }

View File

@ -1822,11 +1822,21 @@ impl LspCommand for InlayHints {
async fn response_from_lsp( async fn response_from_lsp(
self, self,
message: Option<Vec<lsp::InlayHint>>, message: Option<Vec<lsp::InlayHint>>,
_: ModelHandle<Project>, project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>, buffer: ModelHandle<Buffer>,
_: LanguageServerId, server_id: LanguageServerId,
cx: AsyncAppContext, mut cx: AsyncAppContext,
) -> Result<Vec<InlayHint>> { ) -> Result<Vec<InlayHint>> {
let (lsp_adapter, _) = language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
// `typescript-language-server` adds padding to the left for type hints, turning
// `const foo: boolean` into `const foo : boolean` which looks odd.
// `rust-analyzer` does not have the padding for this case, and we have to accomodate both.
//
// We could trim the whole string, but being pessimistic on par with the situation above,
// there might be a hint with multiple whitespaces at the end(s) which we need to display properly.
// Hence let's use a heuristic first to handle the most awkward case and look for more.
let force_no_type_left_padding =
lsp_adapter.name.0.as_ref() == "typescript-language-server";
cx.read(|cx| { cx.read(|cx| {
let origin_buffer = buffer.read(cx); let origin_buffer = buffer.read(cx);
Ok(message Ok(message
@ -1840,6 +1850,12 @@ impl LspCommand for InlayHints {
}); });
let position = origin_buffer let position = origin_buffer
.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left); .clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left);
let padding_left =
if force_no_type_left_padding && kind == Some(InlayHintKind::Type) {
false
} else {
lsp_hint.padding_left.unwrap_or(false)
};
InlayHint { InlayHint {
buffer_id: origin_buffer.remote_id(), buffer_id: origin_buffer.remote_id(),
position: if kind == Some(InlayHintKind::Parameter) { position: if kind == Some(InlayHintKind::Parameter) {
@ -1847,7 +1863,7 @@ impl LspCommand for InlayHints {
} else { } else {
origin_buffer.anchor_after(position) origin_buffer.anchor_after(position)
}, },
padding_left: lsp_hint.padding_left.unwrap_or(false), padding_left,
padding_right: lsp_hint.padding_right.unwrap_or(false), padding_right: lsp_hint.padding_right.unwrap_or(false),
label: match lsp_hint.label { label: match lsp_hint.label {
lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s),

View File

@ -777,20 +777,32 @@ impl Project {
} }
let mut language_servers_to_stop = Vec::new(); let mut language_servers_to_stop = Vec::new();
let mut language_servers_to_restart = Vec::new();
let languages = self.languages.to_vec(); let languages = self.languages.to_vec();
let project_settings = settings::get::<ProjectSettings>(cx).clone();
for (worktree_id, started_lsp_name) in self.language_server_ids.keys() { for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
let language = languages.iter().find(|l| { let language = languages.iter().find_map(|l| {
l.lsp_adapters() let adapter = l
.lsp_adapters()
.iter() .iter()
.any(|adapter| &adapter.name == started_lsp_name) .find(|adapter| &adapter.name == started_lsp_name)?;
Some((l, adapter))
}); });
if let Some(language) = language { if let Some((language, adapter)) = language {
let worktree = self.worktree_for_id(*worktree_id, cx); let worktree = self.worktree_for_id(*worktree_id, cx);
let file = worktree.and_then(|tree| { let file = worktree.as_ref().and_then(|tree| {
tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _)) tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _))
}); });
if !language_settings(Some(language), file.as_ref(), cx).enable_language_server { if !language_settings(Some(language), file.as_ref(), cx).enable_language_server {
language_servers_to_stop.push((*worktree_id, started_lsp_name.clone())); language_servers_to_stop.push((*worktree_id, started_lsp_name.clone()));
} else if let Some(worktree) = worktree {
let new_lsp_settings = project_settings
.lsp
.get(&adapter.name.0)
.and_then(|s| s.initialization_options.as_ref());
if adapter.initialization_options.as_ref() != new_lsp_settings {
language_servers_to_restart.push((worktree, Arc::clone(language)));
}
} }
} }
} }
@ -807,6 +819,11 @@ impl Project {
self.start_language_servers(&worktree, worktree_path, language, cx); self.start_language_servers(&worktree, worktree_path, language, cx);
} }
// Restart all language servers with changed initialization options.
for (worktree, language) in language_servers_to_restart {
self.restart_language_servers(worktree, language, cx);
}
if !self.copilot_enabled && Copilot::global(cx).is_some() { if !self.copilot_enabled && Copilot::global(cx).is_some() {
self.copilot_enabled = true; self.copilot_enabled = true;
for buffer in self.opened_buffers.values() { for buffer in self.opened_buffers.values() {
@ -3397,6 +3414,7 @@ impl Project {
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
cx.emit(Event::RefreshInlays);
status.pending_work.remove(&token); status.pending_work.remove(&token);
cx.notify(); cx.notify();
} }

View File

@ -981,6 +981,19 @@ impl LocalWorktree {
}) })
} }
/// Find the lowest path in the worktree's datastructures that is an ancestor
fn lowest_ancestor(&self, path: &Path) -> PathBuf {
let mut lowest_ancestor = None;
for path in path.ancestors() {
if self.entry_for_path(path).is_some() {
lowest_ancestor = Some(path.to_path_buf());
break;
}
}
lowest_ancestor.unwrap_or_else(|| PathBuf::from(""))
}
pub fn create_entry( pub fn create_entry(
&self, &self,
path: impl Into<Arc<Path>>, path: impl Into<Arc<Path>>,
@ -988,6 +1001,7 @@ impl LocalWorktree {
cx: &mut ModelContext<Worktree>, cx: &mut ModelContext<Worktree>,
) -> Task<Result<Entry>> { ) -> Task<Result<Entry>> {
let path = path.into(); let path = path.into();
let lowest_ancestor = self.lowest_ancestor(&path);
let abs_path = self.absolutize(&path); let abs_path = self.absolutize(&path);
let fs = self.fs.clone(); let fs = self.fs.clone();
let write = cx.background().spawn(async move { let write = cx.background().spawn(async move {
@ -1001,10 +1015,31 @@ impl LocalWorktree {
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
write.await?; write.await?;
this.update(&mut cx, |this, cx| { let (result, refreshes) = this.update(&mut cx, |this, cx| {
this.as_local_mut().unwrap().refresh_entry(path, None, cx) let mut refreshes = Vec::<Task<anyhow::Result<Entry>>>::new();
}) let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap();
.await for refresh_path in refresh_paths.ancestors() {
if refresh_path == Path::new("") {
continue;
}
let refresh_full_path = lowest_ancestor.join(refresh_path);
refreshes.push(this.as_local_mut().unwrap().refresh_entry(
refresh_full_path.into(),
None,
cx,
));
}
(
this.as_local_mut().unwrap().refresh_entry(path, None, cx),
refreshes,
)
});
for refresh in refreshes {
refresh.await.log_err();
}
result.await
}) })
} }
@ -2140,6 +2175,7 @@ impl LocalSnapshot {
impl BackgroundScannerState { impl BackgroundScannerState {
fn should_scan_directory(&self, entry: &Entry) -> bool { fn should_scan_directory(&self, entry: &Entry) -> bool {
(!entry.is_external && !entry.is_ignored) (!entry.is_external && !entry.is_ignored)
|| entry.path.file_name() == Some(&*DOT_GIT)
|| self.scanned_dirs.contains(&entry.id) // If we've ever scanned it, keep scanning || self.scanned_dirs.contains(&entry.id) // If we've ever scanned it, keep scanning
|| self || self
.paths_to_scan .paths_to_scan
@ -2319,6 +2355,7 @@ impl BackgroundScannerState {
.entry_for_id(entry_id) .entry_for_id(entry_id)
.map(|entry| RepositoryWorkDirectory(entry.path.clone())) else { continue }; .map(|entry| RepositoryWorkDirectory(entry.path.clone())) else { continue };
log::info!("reload git repository {:?}", dot_git_dir);
let repository = repository.repo_ptr.lock(); let repository = repository.repo_ptr.lock();
let branch = repository.branch_name(); let branch = repository.branch_name();
repository.reload_index(); repository.reload_index();
@ -2359,6 +2396,8 @@ impl BackgroundScannerState {
} }
fn build_repository(&mut self, dot_git_path: Arc<Path>, fs: &dyn Fs) -> Option<()> { fn build_repository(&mut self, dot_git_path: Arc<Path>, fs: &dyn Fs) -> Option<()> {
log::info!("build git repository {:?}", dot_git_path);
let work_dir_path: Arc<Path> = dot_git_path.parent().unwrap().into(); let work_dir_path: Arc<Path> = dot_git_path.parent().unwrap().into();
// Guard against repositories inside the repository metadata // Guard against repositories inside the repository metadata
@ -3138,8 +3177,6 @@ impl BackgroundScanner {
} }
async fn process_events(&mut self, mut abs_paths: Vec<PathBuf>) { async fn process_events(&mut self, mut abs_paths: Vec<PathBuf>) {
log::debug!("received fs events {:?}", abs_paths);
let root_path = self.state.lock().snapshot.abs_path.clone(); let root_path = self.state.lock().snapshot.abs_path.clone();
let root_canonical_path = match self.fs.canonicalize(&root_path).await { let root_canonical_path = match self.fs.canonicalize(&root_path).await {
Ok(path) => path, Ok(path) => path,
@ -3150,7 +3187,6 @@ impl BackgroundScanner {
}; };
let mut relative_paths = Vec::with_capacity(abs_paths.len()); let mut relative_paths = Vec::with_capacity(abs_paths.len());
let mut unloaded_relative_paths = Vec::new();
abs_paths.sort_unstable(); abs_paths.sort_unstable();
abs_paths.dedup_by(|a, b| a.starts_with(&b)); abs_paths.dedup_by(|a, b| a.starts_with(&b));
abs_paths.retain(|abs_path| { abs_paths.retain(|abs_path| {
@ -3173,7 +3209,6 @@ impl BackgroundScanner {
}); });
if !parent_dir_is_loaded { if !parent_dir_is_loaded {
log::debug!("ignoring event {relative_path:?} within unloaded directory"); log::debug!("ignoring event {relative_path:?} within unloaded directory");
unloaded_relative_paths.push(relative_path);
return false; return false;
} }
@ -3182,7 +3217,12 @@ impl BackgroundScanner {
} }
}); });
if !relative_paths.is_empty() { if relative_paths.is_empty() {
return;
}
log::debug!("received fs events {:?}", relative_paths);
let (scan_job_tx, scan_job_rx) = channel::unbounded(); let (scan_job_tx, scan_job_rx) = channel::unbounded();
self.reload_entries_for_paths( self.reload_entries_for_paths(
root_path, root_path,
@ -3198,11 +3238,9 @@ impl BackgroundScanner {
let (scan_job_tx, scan_job_rx) = channel::unbounded(); let (scan_job_tx, scan_job_rx) = channel::unbounded();
self.update_ignore_statuses(scan_job_tx).await; self.update_ignore_statuses(scan_job_tx).await;
self.scan_dirs(false, scan_job_rx).await; self.scan_dirs(false, scan_job_rx).await;
}
{ {
let mut state = self.state.lock(); let mut state = self.state.lock();
relative_paths.extend(unloaded_relative_paths);
state.reload_repositories(&relative_paths, self.fs.as_ref()); state.reload_repositories(&relative_paths, self.fs.as_ref());
state.snapshot.completed_scan_id = state.snapshot.scan_id; state.snapshot.completed_scan_id = state.snapshot.scan_id;
for (_, entry_id) in mem::take(&mut state.removed_entry_ids) { for (_, entry_id) in mem::take(&mut state.removed_entry_ids) {
@ -3610,11 +3648,11 @@ impl BackgroundScanner {
} }
} }
let fs_entry = state.insert_entry(fs_entry, self.fs.as_ref()); if let (Some(scan_queue_tx), true) = (&scan_queue_tx, fs_entry.is_dir()) {
if state.should_scan_directory(&fs_entry) {
if let Some(scan_queue_tx) = &scan_queue_tx { let mut ancestor_inodes =
let mut ancestor_inodes = state.snapshot.ancestor_inodes_for_path(&path); state.snapshot.ancestor_inodes_for_path(&path);
if metadata.is_dir && !ancestor_inodes.contains(&metadata.inode) { if !ancestor_inodes.contains(&metadata.inode) {
ancestor_inodes.insert(metadata.inode); ancestor_inodes.insert(metadata.inode);
smol::block_on(scan_queue_tx.send(ScanJob { smol::block_on(scan_queue_tx.send(ScanJob {
abs_path, abs_path,
@ -3626,8 +3664,13 @@ impl BackgroundScanner {
})) }))
.unwrap(); .unwrap();
} }
} else {
fs_entry.kind = EntryKind::UnloadedDir;
} }
} }
state.insert_entry(fs_entry, self.fs.as_ref());
}
Ok(None) => { Ok(None) => {
self.remove_repo_path(&path, &mut state.snapshot); self.remove_repo_path(&path, &mut state.snapshot);
} }

View File

@ -936,6 +936,119 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
); );
} }
#[gpui::test]
async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let fs_fake = FakeFs::new(cx.background());
fs_fake
.insert_tree(
"/root",
json!({
"a": {},
}),
)
.await;
let tree_fake = Worktree::local(
client_fake,
"/root".as_ref(),
true,
fs_fake,
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
let entry = tree_fake
.update(cx, |tree, cx| {
tree.as_local_mut()
.unwrap()
.create_entry("a/b/c/d.txt".as_ref(), false, cx)
})
.await
.unwrap();
assert!(entry.is_file());
cx.foreground().run_until_parked();
tree_fake.read_with(cx, |tree, _| {
assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
});
let client_real = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let fs_real = Arc::new(RealFs);
let temp_root = temp_tree(json!({
"a": {}
}));
let tree_real = Worktree::local(
client_real,
temp_root.path(),
true,
fs_real,
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
let entry = tree_real
.update(cx, |tree, cx| {
tree.as_local_mut()
.unwrap()
.create_entry("a/b/c/d.txt".as_ref(), false, cx)
})
.await
.unwrap();
assert!(entry.is_file());
cx.foreground().run_until_parked();
tree_real.read_with(cx, |tree, _| {
assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
});
// Test smallest change
let entry = tree_real
.update(cx, |tree, cx| {
tree.as_local_mut()
.unwrap()
.create_entry("a/b/c/e.txt".as_ref(), false, cx)
})
.await
.unwrap();
assert!(entry.is_file());
cx.foreground().run_until_parked();
tree_real.read_with(cx, |tree, _| {
assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
});
// Test largest change
let entry = tree_real
.update(cx, |tree, cx| {
tree.as_local_mut()
.unwrap()
.create_entry("d/e/f/g.txt".as_ref(), false, cx)
})
.await
.unwrap();
assert!(entry.is_file());
cx.foreground().run_until_parked();
tree_real.read_with(cx, |tree, _| {
assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
assert!(tree.entry_for_path("d/").unwrap().is_dir());
});
}
#[gpui::test(iterations = 100)] #[gpui::test(iterations = 100)]
async fn test_random_worktree_operations_during_initial_scan( async fn test_random_worktree_operations_during_initial_scan(
cx: &mut TestAppContext, cx: &mut TestAppContext,
@ -1654,6 +1767,23 @@ async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppCont
})); }));
const A_TXT: &'static str = "a.txt";
const B_TXT: &'static str = "b.txt";
const E_TXT: &'static str = "c/d/e.txt";
const F_TXT: &'static str = "f.txt";
const DOTGITIGNORE: &'static str = ".gitignore";
const BUILD_FILE: &'static str = "target/build_file";
let project_path = Path::new("project");
// Set up git repository before creating the worktree.
let work_dir = root.path().join("project");
let mut repo = git_init(work_dir.as_path());
repo.add_ignore_rule(IGNORE_RULE).unwrap();
git_add(A_TXT, &repo);
git_add(E_TXT, &repo);
git_add(DOTGITIGNORE, &repo);
git_commit("Initial commit", &repo);
let tree = Worktree::local( let tree = Worktree::local(
build_client(cx), build_client(cx),
root.path(), root.path(),
@ -1665,26 +1795,9 @@ async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppCont
.await .await
.unwrap(); .unwrap();
tree.flush_fs_events(cx).await;
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await; .await;
const A_TXT: &'static str = "a.txt";
const B_TXT: &'static str = "b.txt";
const E_TXT: &'static str = "c/d/e.txt";
const F_TXT: &'static str = "f.txt";
const DOTGITIGNORE: &'static str = ".gitignore";
const BUILD_FILE: &'static str = "target/build_file";
let project_path: &Path = &Path::new("project");
let work_dir = root.path().join("project");
let mut repo = git_init(work_dir.as_path());
repo.add_ignore_rule(IGNORE_RULE).unwrap();
git_add(Path::new(A_TXT), &repo);
git_add(Path::new(E_TXT), &repo);
git_add(Path::new(DOTGITIGNORE), &repo);
git_commit("Initial commit", &repo);
tree.flush_fs_events(cx).await;
deterministic.run_until_parked(); deterministic.run_until_parked();
// Check that the right git state is observed on startup // Check that the right git state is observed on startup
@ -1704,39 +1817,39 @@ async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppCont
); );
}); });
// Modify a file in the working copy.
std::fs::write(work_dir.join(A_TXT), "aa").unwrap(); std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
tree.flush_fs_events(cx).await; tree.flush_fs_events(cx).await;
deterministic.run_until_parked(); deterministic.run_until_parked();
// The worktree detects that the file's git status has changed.
tree.read_with(cx, |tree, _cx| { tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot(); let snapshot = tree.snapshot();
assert_eq!( assert_eq!(
snapshot.status_for_file(project_path.join(A_TXT)), snapshot.status_for_file(project_path.join(A_TXT)),
Some(GitFileStatus::Modified) Some(GitFileStatus::Modified)
); );
}); });
git_add(Path::new(A_TXT), &repo); // Create a commit in the git repository.
git_add(Path::new(B_TXT), &repo); git_add(A_TXT, &repo);
git_add(B_TXT, &repo);
git_commit("Committing modified and added", &repo); git_commit("Committing modified and added", &repo);
tree.flush_fs_events(cx).await; tree.flush_fs_events(cx).await;
deterministic.run_until_parked(); deterministic.run_until_parked();
// Check that repo only changes are tracked // The worktree detects that the files' git status have changed.
tree.read_with(cx, |tree, _cx| { tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot(); let snapshot = tree.snapshot();
assert_eq!( assert_eq!(
snapshot.status_for_file(project_path.join(F_TXT)), snapshot.status_for_file(project_path.join(F_TXT)),
Some(GitFileStatus::Added) Some(GitFileStatus::Added)
); );
assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None); assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None); assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
}); });
// Modify files in the working copy and perform git operations on other files.
git_reset(0, &repo); git_reset(0, &repo);
git_remove_index(Path::new(B_TXT), &repo); git_remove_index(Path::new(B_TXT), &repo);
git_stash(&mut repo); git_stash(&mut repo);

View File

@ -27,6 +27,7 @@ serde_derive.workspace = true
serde_json.workspace = true serde_json.workspace = true
anyhow.workspace = true anyhow.workspace = true
schemars.workspace = true schemars.workspace = true
pretty_assertions.workspace = true
unicase = "2.6" unicase = "2.6"
[dev-dependencies] [dev-dependencies]

View File

@ -64,7 +64,7 @@ pub struct ProjectPanel {
pending_serialization: Task<Option<()>>, pending_serialization: Task<Option<()>>,
} }
#[derive(Copy, Clone)] #[derive(Copy, Clone, Debug)]
struct Selection { struct Selection {
worktree_id: WorktreeId, worktree_id: WorktreeId,
entry_id: ProjectEntryId, entry_id: ProjectEntryId,
@ -547,7 +547,7 @@ impl ProjectPanel {
worktree_id, worktree_id,
entry_id: NEW_ENTRY_ID, entry_id: NEW_ENTRY_ID,
}); });
let new_path = entry.path.join(&filename); let new_path = entry.path.join(&filename.trim_start_matches("/"));
if path_already_exists(new_path.as_path()) { if path_already_exists(new_path.as_path()) {
return None; return None;
} }
@ -588,6 +588,7 @@ impl ProjectPanel {
if selection.entry_id == edited_entry_id { if selection.entry_id == edited_entry_id {
selection.worktree_id = worktree_id; selection.worktree_id = worktree_id;
selection.entry_id = new_entry.id; selection.entry_id = new_entry.id;
this.expand_to_selection(cx);
} }
} }
this.update_visible_entries(None, cx); this.update_visible_entries(None, cx);
@ -965,6 +966,24 @@ impl ProjectPanel {
Some((worktree, entry)) Some((worktree, entry))
} }
fn expand_to_selection(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
let (worktree, entry) = self.selected_entry(cx)?;
let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default();
for path in entry.path.ancestors() {
let Some(entry) = worktree.entry_for_path(path) else {
continue;
};
if entry.is_dir() {
if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) {
expanded_dir_ids.insert(idx, entry.id);
}
}
}
Some(())
}
fn update_visible_entries( fn update_visible_entries(
&mut self, &mut self,
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>, new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
@ -1592,6 +1611,7 @@ impl ClipboardEntry {
mod tests { mod tests {
use super::*; use super::*;
use gpui::{TestAppContext, ViewHandle}; use gpui::{TestAppContext, ViewHandle};
use pretty_assertions::assert_eq;
use project::FakeFs; use project::FakeFs;
use serde_json::json; use serde_json::json;
use settings::SettingsStore; use settings::SettingsStore;
@ -2002,6 +2022,133 @@ mod tests {
); );
} }
#[gpui::test(iterations = 30)]
async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/root1",
json!({
".dockerignore": "",
".git": {
"HEAD": "",
},
"a": {
"0": { "q": "", "r": "", "s": "" },
"1": { "t": "", "u": "" },
"2": { "v": "", "w": "", "x": "", "y": "" },
},
"b": {
"3": { "Q": "" },
"4": { "R": "", "S": "", "T": "", "U": "" },
},
"C": {
"5": {},
"6": { "V": "", "W": "" },
"7": { "X": "" },
"8": { "Y": {}, "Z": "" }
}
}),
)
.await;
fs.insert_tree(
"/root2",
json!({
"d": {
"9": ""
},
"e": {}
}),
)
.await;
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
select_path(&panel, "root1", cx);
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"v root1 <== selected",
" > .git",
" > a",
" > b",
" > C",
" .dockerignore",
"v root2",
" > d",
" > e",
]
);
// Add a file with the root folder selected. The filename editor is placed
// before the first file in the root folder.
panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
cx.read_window(window_id, |cx| {
let panel = panel.read(cx);
assert!(panel.filename_editor.is_focused(cx));
});
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"v root1",
" > .git",
" > a",
" > b",
" > C",
" [EDITOR: ''] <== selected",
" .dockerignore",
"v root2",
" > d",
" > e",
]
);
let confirm = panel.update(cx, |panel, cx| {
panel.filename_editor.update(cx, |editor, cx| {
editor.set_text("/bdir1/dir2/the-new-filename", cx)
});
panel.confirm(&Confirm, cx).unwrap()
});
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"v root1",
" > .git",
" > a",
" > b",
" > C",
" [PROCESSING: '/bdir1/dir2/the-new-filename'] <== selected",
" .dockerignore",
"v root2",
" > d",
" > e",
]
);
confirm.await.unwrap();
assert_eq!(
visible_entries_as_strings(&panel, 0..13, cx),
&[
"v root1",
" > .git",
" > a",
" > b",
" v bdir1",
" v dir2",
" the-new-filename <== selected",
" > C",
" .dockerignore",
"v root2",
" > d",
" > e",
]
);
}
#[gpui::test] #[gpui::test]
async fn test_copy_paste(cx: &mut gpui::TestAppContext) { async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
init_test(cx); init_test(cx);

View File

@ -21,6 +21,7 @@ util = { path = "../util"}
theme = { path = "../theme" } theme = { path = "../theme" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
futures.workspace = true
ordered-float.workspace = true ordered-float.workspace = true
postage.workspace = true postage.workspace = true
smol.workspace = true smol.workspace = true

View File

@ -48,7 +48,7 @@ fn toggle(
let workspace = cx.weak_handle(); let workspace = cx.weak_handle();
cx.add_view(|cx| { cx.add_view(|cx| {
RecentProjects::new( RecentProjects::new(
RecentProjectsDelegate::new(workspace, workspace_locations), RecentProjectsDelegate::new(workspace, workspace_locations, true),
cx, cx,
) )
.with_max_size(800., 1200.) .with_max_size(800., 1200.)
@ -64,25 +64,40 @@ fn toggle(
})) }))
} }
type RecentProjects = Picker<RecentProjectsDelegate>; pub fn build_recent_projects(
workspace: WeakViewHandle<Workspace>,
workspaces: Vec<WorkspaceLocation>,
cx: &mut ViewContext<RecentProjects>,
) -> RecentProjects {
Picker::new(
RecentProjectsDelegate::new(workspace, workspaces, false),
cx,
)
.with_theme(|theme| theme.picker.clone())
}
struct RecentProjectsDelegate { pub type RecentProjects = Picker<RecentProjectsDelegate>;
pub struct RecentProjectsDelegate {
workspace: WeakViewHandle<Workspace>, workspace: WeakViewHandle<Workspace>,
workspace_locations: Vec<WorkspaceLocation>, workspace_locations: Vec<WorkspaceLocation>,
selected_match_index: usize, selected_match_index: usize,
matches: Vec<StringMatch>, matches: Vec<StringMatch>,
render_paths: bool,
} }
impl RecentProjectsDelegate { impl RecentProjectsDelegate {
fn new( fn new(
workspace: WeakViewHandle<Workspace>, workspace: WeakViewHandle<Workspace>,
workspace_locations: Vec<WorkspaceLocation>, workspace_locations: Vec<WorkspaceLocation>,
render_paths: bool,
) -> Self { ) -> Self {
Self { Self {
workspace, workspace,
workspace_locations, workspace_locations,
selected_match_index: 0, selected_match_index: 0,
matches: Default::default(), matches: Default::default(),
render_paths,
} }
} }
} }
@ -188,6 +203,7 @@ impl PickerDelegate for RecentProjectsDelegate {
highlighted_location highlighted_location
.paths .paths
.into_iter() .into_iter()
.filter(|_| self.render_paths)
.map(|highlighted_path| highlighted_path.render(style.label.clone())), .map(|highlighted_path| highlighted_path.render(style.label.clone())),
) )
.flex(1., false) .flex(1., false)

View File

@ -675,6 +675,9 @@ impl ProjectSearchView {
if match_ranges.is_empty() { if match_ranges.is_empty() {
self.active_match_index = None; self.active_match_index = None;
} else { } else {
self.active_match_index = Some(0);
self.select_match(Direction::Next, cx);
self.update_match_index(cx);
let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id); let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id);
let is_new_search = self.search_id != prev_search_id; let is_new_search = self.search_id != prev_search_id;
self.results_editor.update(cx, |editor, cx| { self.results_editor.update(cx, |editor, cx| {

View File

@ -38,5 +38,5 @@ tree-sitter-json = "*"
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
fs = { path = "../fs", features = ["test-support"] } fs = { path = "../fs", features = ["test-support"] }
indoc.workspace = true indoc.workspace = true
pretty_assertions = "1.3.0" pretty_assertions.workspace = true
unindent.workspace = true unindent.workspace = true

View File

@ -1,7 +1,7 @@
use crate::{settings_store::parse_json_with_comments, SettingsAssets}; use crate::{settings_store::parse_json_with_comments, SettingsAssets};
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use collections::BTreeMap; use collections::BTreeMap;
use gpui::{keymap_matcher::Binding, AppContext}; use gpui::{keymap_matcher::Binding, AppContext, NoAction};
use schemars::{ use schemars::{
gen::{SchemaGenerator, SchemaSettings}, gen::{SchemaGenerator, SchemaSettings},
schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation}, schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},
@ -11,18 +11,18 @@ use serde::Deserialize;
use serde_json::Value; use serde_json::Value;
use util::{asset_str, ResultExt}; use util::{asset_str, ResultExt};
#[derive(Deserialize, Default, Clone, JsonSchema)] #[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
#[serde(transparent)] #[serde(transparent)]
pub struct KeymapFile(Vec<KeymapBlock>); pub struct KeymapFile(Vec<KeymapBlock>);
#[derive(Deserialize, Default, Clone, JsonSchema)] #[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
pub struct KeymapBlock { pub struct KeymapBlock {
#[serde(default)] #[serde(default)]
context: Option<String>, context: Option<String>,
bindings: BTreeMap<String, KeymapAction>, bindings: BTreeMap<String, KeymapAction>,
} }
#[derive(Deserialize, Default, Clone)] #[derive(Debug, Deserialize, Default, Clone)]
#[serde(transparent)] #[serde(transparent)]
pub struct KeymapAction(Value); pub struct KeymapAction(Value);
@ -61,7 +61,8 @@ impl KeymapFile {
// We want to deserialize the action data as a `RawValue` so that we can // We want to deserialize the action data as a `RawValue` so that we can
// deserialize the action itself dynamically directly from the JSON // deserialize the action itself dynamically directly from the JSON
// string. But `RawValue` currently does not work inside of an untagged enum. // string. But `RawValue` currently does not work inside of an untagged enum.
if let Value::Array(items) = action { match action {
Value::Array(items) => {
let Ok([name, data]): Result<[serde_json::Value; 2], _> = items.try_into() else { let Ok([name, data]): Result<[serde_json::Value; 2], _> = items.try_into() else {
return Some(Err(anyhow!("Expected array of length 2"))); return Some(Err(anyhow!("Expected array of length 2")));
}; };
@ -72,10 +73,10 @@ impl KeymapFile {
&name, &name,
Some(data), Some(data),
) )
} else if let Value::String(name) = action { },
cx.deserialize_action(&name, None) Value::String(name) => cx.deserialize_action(&name, None),
} else { Value::Null => Ok(no_action()),
return Some(Err(anyhow!("Expected two-element array, got {:?}", action))); _ => return Some(Err(anyhow!("Expected two-element array, got {action:?}"))),
} }
.with_context(|| { .with_context(|| {
format!( format!(
@ -115,6 +116,10 @@ impl KeymapFile {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))), instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
..Default::default() ..Default::default()
}), }),
Schema::Object(SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Null))),
..Default::default()
}),
]), ]),
..Default::default() ..Default::default()
})), })),
@ -129,6 +134,10 @@ impl KeymapFile {
} }
} }
fn no_action() -> Box<dyn gpui::Action> {
Box::new(NoAction {})
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::KeymapFile; use crate::KeymapFile;

View File

@ -395,16 +395,17 @@ impl TerminalElement {
// Terminal Emulator controlled behavior: // Terminal Emulator controlled behavior:
region = region region = region
// Start selections // Start selections
.on_down( .on_down(MouseButton::Left, move |event, v: &mut TerminalView, cx| {
MouseButton::Left, cx.focus_parent();
TerminalElement::generic_button_handler( v.context_menu.update(cx, |menu, _cx| menu.delay_cancel());
connection, if let Some(conn_handle) = connection.upgrade(cx) {
origin, conn_handle.update(cx, |terminal, cx| {
move |terminal, origin, e, _cx| { terminal.mouse_down(&event, origin);
terminal.mouse_down(&e, origin);
}, cx.notify();
), })
) }
})
// Update drag selections // Update drag selections
.on_drag(MouseButton::Left, move |event, _: &mut TerminalView, cx| { .on_drag(MouseButton::Left, move |event, _: &mut TerminalView, cx| {
if cx.is_self_focused() { if cx.is_self_focused() {

View File

@ -87,6 +87,7 @@ impl TerminalPanel {
} }
}) })
}, },
|_, _| {},
None, None,
)) ))
.with_child(Pane::render_tab_bar_button( .with_child(Pane::render_tab_bar_button(
@ -100,6 +101,7 @@ impl TerminalPanel {
Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))), Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))),
cx, cx,
move |pane, cx| pane.toggle_zoom(&Default::default(), cx), move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
|_, _| {},
None, None,
)) ))
.into_any() .into_any()

View File

@ -2489,7 +2489,12 @@ impl ToOffset for Point {
impl ToOffset for usize { impl ToOffset for usize {
fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { fn to_offset(&self, snapshot: &BufferSnapshot) -> usize {
assert!(*self <= snapshot.len(), "offset {self} is out of range"); assert!(
*self <= snapshot.len(),
"offset {} is out of range, max allowed is {}",
self,
snapshot.len()
);
*self *self
} }
} }

View File

@ -65,7 +65,6 @@ pub struct Theme {
pub assistant: AssistantStyle, pub assistant: AssistantStyle,
pub feedback: FeedbackStyle, pub feedback: FeedbackStyle,
pub welcome: WelcomeStyle, pub welcome: WelcomeStyle,
pub color_scheme: ColorScheme,
pub titlebar: Titlebar, pub titlebar: Titlebar,
} }
@ -118,8 +117,9 @@ pub struct Titlebar {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
pub height: f32, pub height: f32,
pub title: TextStyle, pub project_menu_button: Toggleable<Interactive<ContainedText>>,
pub highlight_color: Color, pub project_name_divider: ContainedText,
pub git_menu_button: Toggleable<Interactive<ContainedText>>,
pub item_spacing: f32, pub item_spacing: f32,
pub face_pile_spacing: f32, pub face_pile_spacing: f32,
pub avatar_ribbon: AvatarRibbon, pub avatar_ribbon: AvatarRibbon,
@ -585,6 +585,8 @@ pub struct Picker {
pub empty_input_editor: FieldEditor, pub empty_input_editor: FieldEditor,
pub no_matches: ContainedLabel, pub no_matches: ContainedLabel,
pub item: Toggleable<Interactive<ContainedLabel>>, pub item: Toggleable<Interactive<ContainedLabel>>,
pub header: ContainedLabel,
pub footer: ContainedLabel,
} }
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)] #[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
@ -720,6 +722,7 @@ pub struct Scrollbar {
pub width: f32, pub width: f32,
pub min_height_factor: f32, pub min_height_factor: f32,
pub git: GitDiffColors, pub git: GitDiffColors,
pub selections: Color,
} }
#[derive(Clone, Deserialize, Default, JsonSchema)] #[derive(Clone, Deserialize, Default, JsonSchema)]

View File

@ -36,7 +36,6 @@ workspace = { path = "../workspace" }
[dev-dependencies] [dev-dependencies]
indoc.workspace = true indoc.workspace = true
parking_lot.workspace = true parking_lot.workspace = true
lazy_static.workspace = true
editor = { path = "../editor", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }

View File

@ -31,6 +31,8 @@ pub enum Motion {
CurrentLine, CurrentLine,
StartOfLine, StartOfLine,
EndOfLine, EndOfLine,
StartOfParagraph,
EndOfParagraph,
StartOfDocument, StartOfDocument,
EndOfDocument, EndOfDocument,
Matching, Matching,
@ -72,6 +74,8 @@ actions!(
StartOfLine, StartOfLine,
EndOfLine, EndOfLine,
CurrentLine, CurrentLine,
StartOfParagraph,
EndOfParagraph,
StartOfDocument, StartOfDocument,
EndOfDocument, EndOfDocument,
Matching, Matching,
@ -92,6 +96,12 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx)); cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx));
cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx)); cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx));
cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx)); cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx));
cx.add_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
motion(Motion::StartOfParagraph, cx)
});
cx.add_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
motion(Motion::EndOfParagraph, cx)
});
cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| { cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
motion(Motion::StartOfDocument, cx) motion(Motion::StartOfDocument, cx)
}); });
@ -142,7 +152,8 @@ impl Motion {
pub fn linewise(&self) -> bool { pub fn linewise(&self) -> bool {
use Motion::*; use Motion::*;
match self { match self {
Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart => true, Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart
| StartOfParagraph | EndOfParagraph => true,
EndOfLine EndOfLine
| NextWordEnd { .. } | NextWordEnd { .. }
| Matching | Matching
@ -172,6 +183,8 @@ impl Motion {
| Backspace | Backspace
| Right | Right
| StartOfLine | StartOfLine
| StartOfParagraph
| EndOfParagraph
| NextWordStart { .. } | NextWordStart { .. }
| PreviousWordStart { .. } | PreviousWordStart { .. }
| FirstNonWhitespace | FirstNonWhitespace
@ -197,6 +210,8 @@ impl Motion {
| Backspace | Backspace
| Right | Right
| StartOfLine | StartOfLine
| StartOfParagraph
| EndOfParagraph
| NextWordStart { .. } | NextWordStart { .. }
| PreviousWordStart { .. } | PreviousWordStart { .. }
| FirstNonWhitespace | FirstNonWhitespace
@ -235,6 +250,14 @@ impl Motion {
FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None), FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None),
StartOfLine => (start_of_line(map, point), SelectionGoal::None), StartOfLine => (start_of_line(map, point), SelectionGoal::None),
EndOfLine => (end_of_line(map, point), SelectionGoal::None), EndOfLine => (end_of_line(map, point), SelectionGoal::None),
StartOfParagraph => (
movement::start_of_paragraph(map, point, times),
SelectionGoal::None,
),
EndOfParagraph => (
map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
SelectionGoal::None,
),
CurrentLine => (end_of_line(map, point), SelectionGoal::None), CurrentLine => (end_of_line(map, point), SelectionGoal::None),
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None), StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
EndOfDocument => ( EndOfDocument => (
@ -502,10 +525,13 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint
if line_end == point { if line_end == point {
line_end = map.max_point().to_point(map); line_end = map.max_point().to_point(map);
} }
line_end.column = line_end.column.saturating_sub(1);
let line_range = map.prev_line_boundary(point).0..line_end; let line_range = map.prev_line_boundary(point).0..line_end;
let ranges = map.buffer_snapshot.bracket_ranges(line_range.clone()); let visible_line_range =
line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
let ranges = map
.buffer_snapshot
.bracket_ranges(visible_line_range.clone());
if let Some(ranges) = ranges { if let Some(ranges) = ranges {
let line_range = line_range.start.to_offset(&map.buffer_snapshot) let line_range = line_range.start.to_offset(&map.buffer_snapshot)
..line_range.end.to_offset(&map.buffer_snapshot); ..line_range.end.to_offset(&map.buffer_snapshot);
@ -590,3 +616,131 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) ->
let new_row = (point.row() + times as u32).min(map.max_buffer_row()); let new_row = (point.row() + times as u32).min(map.max_buffer_row());
map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left) map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left)
} }
#[cfg(test)]
mod test {
use crate::test::NeovimBackedTestContext;
use indoc::indoc;
#[gpui::test]
async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
let initial_state = indoc! {r"ˇabc
def
paragraph
the second
third and
final"};
// goes down once
cx.set_shared_state(initial_state).await;
cx.simulate_shared_keystrokes(["}"]).await;
cx.assert_shared_state(indoc! {r"abc
def
ˇ
paragraph
the second
third and
final"})
.await;
// goes up once
cx.simulate_shared_keystrokes(["{"]).await;
cx.assert_shared_state(initial_state).await;
// goes down twice
cx.simulate_shared_keystrokes(["2", "}"]).await;
cx.assert_shared_state(indoc! {r"abc
def
paragraph
the second
ˇ
third and
final"})
.await;
// goes down over multiple blanks
cx.simulate_shared_keystrokes(["}"]).await;
cx.assert_shared_state(indoc! {r"abc
def
paragraph
the second
third and
finaˇl"})
.await;
// goes up twice
cx.simulate_shared_keystrokes(["2", "{"]).await;
cx.assert_shared_state(indoc! {r"abc
def
ˇ
paragraph
the second
third and
final"})
.await
}
#[gpui::test]
async fn test_matching(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state(indoc! {r"func ˇ(a string) {
do(something(with<Types>.and_arrays[0, 2]))
}"})
.await;
cx.simulate_shared_keystrokes(["%"]).await;
cx.assert_shared_state(indoc! {r"func (a stringˇ) {
do(something(with<Types>.and_arrays[0, 2]))
}"})
.await;
// test it works on the last character of the line
cx.set_shared_state(indoc! {r"func (a string) ˇ{
do(something(with<Types>.and_arrays[0, 2]))
}"})
.await;
cx.simulate_shared_keystrokes(["%"]).await;
cx.assert_shared_state(indoc! {r"func (a string) {
do(something(with<Types>.and_arrays[0, 2]))
ˇ}"})
.await;
// test it works on immediate nesting
cx.set_shared_state("ˇ{()}").await;
cx.simulate_shared_keystrokes(["%"]).await;
cx.assert_shared_state("{()ˇ}").await;
cx.simulate_shared_keystrokes(["%"]).await;
cx.assert_shared_state("ˇ{()}").await;
// test it works on immediate nesting inside braces
cx.set_shared_state("{\n ˇ{()}\n}").await;
cx.simulate_shared_keystrokes(["%"]).await;
cx.assert_shared_state("{\n {()ˇ}\n}").await;
// test it jumps to the next paren on a line
cx.set_shared_state("func ˇboop() {\n}").await;
cx.simulate_shared_keystrokes(["%"]).await;
cx.assert_shared_state("func boop(ˇ) {\n}").await;
}
}

View File

@ -1,29 +1,51 @@
use editor::scroll::autoscroll::Autoscroll;
use gpui::ViewContext; use gpui::ViewContext;
use language::Point; use language::{Bias, Point};
use workspace::Workspace; use workspace::Workspace;
use crate::{motion::Motion, normal::ChangeCase, Vim}; use crate::{normal::ChangeCase, state::Mode, Vim};
pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) { pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| { Vim::update(cx, |vim, cx| {
let count = vim.pop_number_operator(cx); let count = vim.pop_number_operator(cx).unwrap_or(1) as u32;
vim.update_active_editor(cx, |editor, cx| { vim.update_active_editor(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx); let mut ranges = Vec::new();
editor.transact(cx, |editor, cx| { let mut cursor_positions = Vec::new();
editor.change_selections(None, cx, |s| { let snapshot = editor.buffer().read(cx).snapshot(cx);
s.move_with(|map, selection| { for selection in editor.selections.all::<Point>(cx) {
if selection.start == selection.end { match vim.state.mode {
Motion::Right.expand_selection(map, selection, count, true); Mode::Visual { line: true } => {
let start = Point::new(selection.start.row, 0);
let end =
Point::new(selection.end.row, snapshot.line_len(selection.end.row));
ranges.push(start..end);
cursor_positions.push(start..start);
} }
}) Mode::Visual { line: false } => {
}); ranges.push(selection.start..selection.end);
let selections = editor.selections.all::<Point>(cx); cursor_positions.push(selection.start..selection.start);
for selection in selections.into_iter().rev() { }
Mode::Insert | Mode::Normal => {
let start = selection.start;
let mut end = start;
for _ in 0..count {
end = snapshot.clip_point(end + Point::new(0, 1), Bias::Right);
}
ranges.push(start..end);
if end.column == snapshot.line_len(end.row) {
end = snapshot.clip_point(end - Point::new(0, 1), Bias::Left);
}
cursor_positions.push(end..end)
}
}
}
editor.transact(cx, |editor, cx| {
for range in ranges.into_iter().rev() {
let snapshot = editor.buffer().read(cx).snapshot(cx); let snapshot = editor.buffer().read(cx).snapshot(cx);
editor.buffer().update(cx, |buffer, cx| { editor.buffer().update(cx, |buffer, cx| {
let range = selection.start..selection.end;
let text = snapshot let text = snapshot
.text_for_range(selection.start..selection.end) .text_for_range(range.start..range.end)
.flat_map(|s| s.chars()) .flat_map(|s| s.chars())
.flat_map(|c| { .flat_map(|c| {
if c.is_lowercase() { if c.is_lowercase() {
@ -37,28 +59,46 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Works
buffer.edit([(range, text)], None, cx) buffer.edit([(range, text)], None, cx)
}) })
} }
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(cursor_positions)
})
}); });
editor.set_clip_at_line_ends(true, cx);
}); });
vim.switch_mode(Mode::Normal, true, cx)
}) })
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{state::Mode, test::VimTestContext}; use crate::{state::Mode, test::NeovimBackedTestContext};
use indoc::indoc;
#[gpui::test] #[gpui::test]
async fn test_change_case(cx: &mut gpui::TestAppContext) { async fn test_change_case(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await; let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_state(indoc! {"ˇabC\n"}, Mode::Normal); cx.set_shared_state("ˇabC\n").await;
cx.simulate_keystrokes(["~"]); cx.simulate_shared_keystrokes(["~"]).await;
cx.assert_editor_state("AˇbC\n"); cx.assert_shared_state("AˇbC\n").await;
cx.simulate_keystrokes(["2", "~"]); cx.simulate_shared_keystrokes(["2", "~"]).await;
cx.assert_editor_state("ABcˇ\n"); cx.assert_shared_state("ABˇc\n").await;
cx.set_state(indoc! {"a😀C«dÉ1*fˇ»\n"}, Mode::Normal); // works in visual mode
cx.simulate_keystrokes(["~"]); cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
cx.assert_editor_state("a😀CDé1*Fˇ\n"); cx.simulate_shared_keystrokes(["~"]).await;
cx.assert_shared_state("a😀CˇDé1*F\n").await;
// works with multibyte characters
cx.simulate_shared_keystrokes(["~"]).await;
cx.set_shared_state("aˇC😀é1*F\n").await;
cx.simulate_shared_keystrokes(["4", "~"]).await;
cx.assert_shared_state("ac😀É1ˇ*F\n").await;
// works with line selections
cx.set_shared_state("abˇC\n").await;
cx.simulate_shared_keystrokes(["shift-v", "~"]).await;
cx.assert_shared_state("ˇABc\n").await;
// works with multiple cursors (zed only)
cx.set_state("aˇßcdˇe\n", Mode::Normal);
cx.simulate_keystroke("~");
cx.assert_state("aSSˇcdˇE\n", Mode::Normal);
} }
} }

View File

@ -4,6 +4,7 @@ mod neovim_connection;
mod vim_binding_test_context; mod vim_binding_test_context;
mod vim_test_context; mod vim_test_context;
use command_palette::CommandPalette;
pub use neovim_backed_binding_test_context::*; pub use neovim_backed_binding_test_context::*;
pub use neovim_backed_test_context::*; pub use neovim_backed_test_context::*;
pub use vim_binding_test_context::*; pub use vim_binding_test_context::*;
@ -139,3 +140,16 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
cx.simulate_keystrokes(["shift-v", "down", ">", ">"]); cx.simulate_keystrokes(["shift-v", "down", ">", ">"]);
cx.assert_editor_state("aa\n b«b\n cˇ»c"); cx.assert_editor_state("aa\n b«b\n cˇ»c");
} }
#[gpui::test]
async fn test_escape_command_palette(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.set_state("aˇbc\n", Mode::Normal);
cx.simulate_keystrokes(["i", "cmd-shift-p"]);
assert!(cx.workspace(|workspace, _| workspace.modal::<CommandPalette>().is_some()));
cx.simulate_keystroke("escape");
assert!(!cx.workspace(|workspace, _| workspace.modal::<CommandPalette>().is_some()));
cx.assert_state("aˇbc\n", Mode::Insert);
}

View File

@ -1,9 +1,10 @@
use std::ops::{Deref, DerefMut}; use indoc::indoc;
use std::ops::{Deref, DerefMut, Range};
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use gpui::ContextHandle; use gpui::ContextHandle;
use language::OffsetRangeExt; use language::OffsetRangeExt;
use util::test::marked_text_offsets; use util::test::{generate_marked_text, marked_text_offsets};
use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext}; use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
use crate::state::Mode; use crate::state::Mode;
@ -112,6 +113,43 @@ impl<'a> NeovimBackedTestContext<'a> {
context_handle context_handle
} }
pub async fn assert_shared_state(&mut self, marked_text: &str) {
let neovim = self.neovim_state().await;
if neovim != marked_text {
panic!(
indoc! {"Test is incorrect (currently expected != neovim state)
# currently expected:
{}
# neovim state:
{}
# zed state:
{}"},
marked_text,
neovim,
self.editor_state(),
)
}
self.assert_editor_state(marked_text)
}
pub async fn neovim_state(&mut self) -> String {
generate_marked_text(
self.neovim.text().await.as_str(),
&vec![self.neovim_selection().await],
true,
)
}
async fn neovim_selection(&mut self) -> Range<usize> {
let mut neovim_selection = self.neovim.selection().await;
// Zed selections adjust themselves to make the end point visually make sense
if neovim_selection.start > neovim_selection.end {
neovim_selection.start.column += 1;
}
neovim_selection.to_offset(&self.buffer_snapshot())
}
pub async fn assert_state_matches(&mut self) { pub async fn assert_state_matches(&mut self) {
assert_eq!( assert_eq!(
self.neovim.text().await, self.neovim.text().await,
@ -120,13 +158,8 @@ impl<'a> NeovimBackedTestContext<'a> {
self.assertion_context() self.assertion_context()
); );
let mut neovim_selection = self.neovim.selection().await; let selections = vec![self.neovim_selection().await];
// Zed selections adjust themselves to make the end point visually make sense self.assert_editor_selections(selections);
if neovim_selection.start > neovim_selection.end {
neovim_selection.start.column += 1;
}
let neovim_selection = neovim_selection.to_offset(&self.buffer_snapshot());
self.assert_editor_selections(vec![neovim_selection]);
if let Some(neovim_mode) = self.neovim.mode().await { if let Some(neovim_mode) = self.neovim.mode().await {
assert_eq!(neovim_mode, self.mode(), "{}", self.assertion_context(),); assert_eq!(neovim_mode, self.mode(), "{}", self.assertion_context(),);

View File

@ -11,8 +11,6 @@ use gpui::keymap_matcher::Keystroke;
use language::Point; use language::Point;
#[cfg(feature = "neovim")]
use lazy_static::lazy_static;
#[cfg(feature = "neovim")] #[cfg(feature = "neovim")]
use nvim_rs::{ use nvim_rs::{
create::tokio::new_child_cmd, error::LoopError, Handler, Neovim, UiAttachOptions, Value, create::tokio::new_child_cmd, error::LoopError, Handler, Neovim, UiAttachOptions, Value,
@ -32,9 +30,7 @@ use collections::VecDeque;
// Neovim doesn't like to be started simultaneously from multiple threads. We use this lock // Neovim doesn't like to be started simultaneously from multiple threads. We use this lock
// to ensure we are only constructing one neovim connection at a time. // to ensure we are only constructing one neovim connection at a time.
#[cfg(feature = "neovim")] #[cfg(feature = "neovim")]
lazy_static! { static NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
static ref NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum NeovimData { pub enum NeovimData {
@ -171,15 +167,25 @@ impl NeovimConnection {
.await .await
.expect("Could not get neovim window"); .expect("Could not get neovim window");
if !selection.is_empty() {
panic!("Setting neovim state with non empty selection not yet supported");
}
let cursor = selection.start; let cursor = selection.start;
nvim_window nvim_window
.set_cursor((cursor.row as i64 + 1, cursor.column as i64)) .set_cursor((cursor.row as i64 + 1, cursor.column as i64))
.await .await
.expect("Could not set nvim cursor position"); .expect("Could not set nvim cursor position");
if !selection.is_empty() {
self.nvim
.input("v")
.await
.expect("could not enter visual mode");
let cursor = selection.end;
nvim_window
.set_cursor((cursor.row as i64 + 1, cursor.column as i64))
.await
.expect("Could not set nvim cursor position");
}
if let Some(NeovimData::Get { mode, state }) = self.data.back() { if let Some(NeovimData::Get { mode, state }) = self.data.back() {
if *mode == Some(Mode::Normal) && *state == marked_text { if *mode == Some(Mode::Normal) && *state == marked_text {
return; return;

View File

@ -21,12 +21,14 @@ impl<'a> VimTestContext<'a> {
cx.update(|cx| { cx.update(|cx| {
search::init(cx); search::init(cx);
crate::init(cx); crate::init(cx);
command_palette::init(cx);
}); });
cx.update(|cx| { cx.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| { cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled)); store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
}); });
settings::KeymapFile::load_asset("keymaps/default.json", cx).unwrap();
settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap(); settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
}); });

View File

@ -12,7 +12,7 @@ mod visual;
use anyhow::Result; use anyhow::Result;
use collections::CommandPaletteFilter; use collections::CommandPaletteFilter;
use editor::{Bias, Cancel, Editor, EditorMode, Event}; use editor::{Bias, Editor, EditorMode, Event};
use gpui::{ use gpui::{
actions, impl_actions, AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle, actions, impl_actions, AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle,
WindowContext, WindowContext,
@ -64,22 +64,6 @@ pub fn init(cx: &mut AppContext) {
Vim::update(cx, |vim, cx| vim.push_number(n, cx)); Vim::update(cx, |vim, cx| vim.push_number(n, cx));
}); });
// Editor Actions
cx.add_action(|_: &mut Editor, _: &Cancel, cx| {
// If we are in aren't in normal mode or have an active operator, swap to normal mode
// Otherwise forward cancel on to the editor
let vim = Vim::read(cx);
if vim.state.mode != Mode::Normal || vim.active_operator().is_some() {
WindowContext::defer(cx, |cx| {
Vim::update(cx, |state, cx| {
state.switch_mode(Mode::Normal, false, cx);
});
});
} else {
cx.propagate_action();
}
});
cx.add_action(|_: &mut Workspace, _: &Tab, cx| { cx.add_action(|_: &mut Workspace, _: &Tab, cx| {
Vim::active_editor_input_ignored(" ".into(), cx) Vim::active_editor_input_ignored(" ".into(), cx)
}); });
@ -109,10 +93,7 @@ pub fn observe_keystrokes(cx: &mut WindowContext) {
cx.observe_keystrokes(|_keystroke, _result, handled_by, cx| { cx.observe_keystrokes(|_keystroke, _result, handled_by, cx| {
if let Some(handled_by) = handled_by { if let Some(handled_by) = handled_by {
// Keystroke is handled by the vim system, so continue forward // Keystroke is handled by the vim system, so continue forward
// Also short circuit if it is the special cancel action if handled_by.namespace() == "vim" {
if handled_by.namespace() == "vim"
|| (handled_by.namespace() == "editor" && handled_by.name() == "Cancel")
{
return true; return true;
} }
} }

View File

@ -0,0 +1,18 @@
{"Put":{"state":"ˇabC\n"}}
{"Key":"~"}
{"Get":{"state":"AˇbC\n","mode":"Normal"}}
{"Key":"2"}
{"Key":"~"}
{"Get":{"state":"ABˇc\n","mode":"Normal"}}
{"Put":{"state":"a😀C«dÉ1*fˇ»\n"}}
{"Key":"~"}
{"Get":{"state":"a😀CˇDé1*F\n","mode":"Normal"}}
{"Key":"~"}
{"Put":{"state":"aˇC😀é1*F\n"}}
{"Key":"4"}
{"Key":"~"}
{"Get":{"state":"ac😀É1ˇ*F\n","mode":"Normal"}}
{"Put":{"state":"abˇC\n"}}
{"Key":"shift-v"}
{"Key":"~"}
{"Get":{"state":"ˇABc\n","mode":"Normal"}}

View File

@ -0,0 +1,17 @@
{"Put":{"state":"func ˇ(a string) {\n do(something(with<Types>.and_arrays[0, 2]))\n}"}}
{"Key":"%"}
{"Get":{"state":"func (a stringˇ) {\n do(something(with<Types>.and_arrays[0, 2]))\n}","mode":"Normal"}}
{"Put":{"state":"func (a string) ˇ{\ndo(something(with<Types>.and_arrays[0, 2]))\n}"}}
{"Key":"%"}
{"Get":{"state":"func (a string) {\ndo(something(with<Types>.and_arrays[0, 2]))\nˇ}","mode":"Normal"}}
{"Put":{"state":"ˇ{()}"}}
{"Key":"%"}
{"Get":{"state":"{()ˇ}","mode":"Normal"}}
{"Key":"%"}
{"Get":{"state":"ˇ{()}","mode":"Normal"}}
{"Put":{"state":"{\n ˇ{()}\n}"}}
{"Key":"%"}
{"Get":{"state":"{\n {()ˇ}\n}","mode":"Normal"}}
{"Put":{"state":"func ˇboop() {\n}"}}
{"Key":"%"}
{"Get":{"state":"func boop(ˇ) {\n}","mode":"Normal"}}

View File

@ -0,0 +1,13 @@
{"Put":{"state":"ˇabc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinal"}}
{"Key":"}"}
{"Get":{"state":"abc\ndef\nˇ\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}}
{"Key":"{"}
{"Get":{"state":"ˇabc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}}
{"Key":"2"}
{"Key":"}"}
{"Get":{"state":"abc\ndef\n\nparagraph\nthe second\nˇ\n\n\nthird and\nfinal","mode":"Normal"}}
{"Key":"}"}
{"Get":{"state":"abc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinaˇl","mode":"Normal"}}
{"Key":"2"}
{"Key":"{"}
{"Get":{"state":"abc\ndef\nˇ\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}}

View File

@ -273,6 +273,11 @@ impl Pane {
Some(("New...".into(), None)), Some(("New...".into(), None)),
cx, cx,
|pane, cx| pane.deploy_new_menu(cx), |pane, cx| pane.deploy_new_menu(cx),
|pane, cx| {
pane.tab_bar_context_menu
.handle
.update(cx, |menu, _| menu.delay_cancel())
},
pane.tab_bar_context_menu pane.tab_bar_context_menu
.handle_if_kind(TabBarContextMenuKind::New), .handle_if_kind(TabBarContextMenuKind::New),
)) ))
@ -283,6 +288,11 @@ impl Pane {
Some(("Split Pane".into(), None)), Some(("Split Pane".into(), None)),
cx, cx,
|pane, cx| pane.deploy_split_menu(cx), |pane, cx| pane.deploy_split_menu(cx),
|pane, cx| {
pane.tab_bar_context_menu
.handle
.update(cx, |menu, _| menu.delay_cancel())
},
pane.tab_bar_context_menu pane.tab_bar_context_menu
.handle_if_kind(TabBarContextMenuKind::Split), .handle_if_kind(TabBarContextMenuKind::Split),
)) ))
@ -304,6 +314,7 @@ impl Pane {
Some((tooltip_label, Some(Box::new(ToggleZoom)))), Some((tooltip_label, Some(Box::new(ToggleZoom)))),
cx, cx,
move |pane, cx| pane.toggle_zoom(&Default::default(), cx), move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
move |_, _| {},
None, None,
) )
}) })
@ -988,7 +999,7 @@ impl Pane {
fn deploy_split_menu(&mut self, cx: &mut ViewContext<Self>) { fn deploy_split_menu(&mut self, cx: &mut ViewContext<Self>) {
self.tab_bar_context_menu.handle.update(cx, |menu, cx| { self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
menu.show( menu.toggle(
Default::default(), Default::default(),
AnchorCorner::TopRight, AnchorCorner::TopRight,
vec![ vec![
@ -1006,7 +1017,7 @@ impl Pane {
fn deploy_new_menu(&mut self, cx: &mut ViewContext<Self>) { fn deploy_new_menu(&mut self, cx: &mut ViewContext<Self>) {
self.tab_bar_context_menu.handle.update(cx, |menu, cx| { self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
menu.show( menu.toggle(
Default::default(), Default::default(),
AnchorCorner::TopRight, AnchorCorner::TopRight,
vec![ vec![
@ -1416,13 +1427,17 @@ impl Pane {
.into_any() .into_any()
} }
pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>( pub fn render_tab_bar_button<
F1: 'static + Fn(&mut Pane, &mut EventContext<Pane>),
F2: 'static + Fn(&mut Pane, &mut EventContext<Pane>),
>(
index: usize, index: usize,
icon: &'static str, icon: &'static str,
is_active: bool, is_active: bool,
tooltip: Option<(String, Option<Box<dyn Action>>)>, tooltip: Option<(String, Option<Box<dyn Action>>)>,
cx: &mut ViewContext<Pane>, cx: &mut ViewContext<Pane>,
on_click: F, on_click: F1,
on_down: F2,
context_menu: Option<ViewHandle<ContextMenu>>, context_menu: Option<ViewHandle<ContextMenu>>,
) -> AnyElement<Pane> { ) -> AnyElement<Pane> {
enum TabBarButton {} enum TabBarButton {}
@ -1440,6 +1455,7 @@ impl Pane {
.with_height(style.button_width) .with_height(style.button_width)
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_down(MouseButton::Left, move |_, pane, cx| on_down(pane, cx))
.on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx)) .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx))
.into_any(); .into_any();
if let Some((tooltip, action)) = tooltip { if let Some((tooltip, action)) = tooltip {

View File

@ -97,9 +97,25 @@ lazy_static! {
} }
pub trait Modal: View { pub trait Modal: View {
fn has_focus(&self) -> bool;
fn dismiss_on_event(event: &Self::Event) -> bool; fn dismiss_on_event(event: &Self::Event) -> bool;
} }
trait ModalHandle {
fn as_any(&self) -> &AnyViewHandle;
fn has_focus(&self, cx: &WindowContext) -> bool;
}
impl<T: Modal> ModalHandle for ViewHandle<T> {
fn as_any(&self) -> &AnyViewHandle {
self
}
fn has_focus(&self, cx: &WindowContext) -> bool {
self.read(cx).has_focus()
}
}
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct RemoveWorktreeFromProject(pub WorktreeId); pub struct RemoveWorktreeFromProject(pub WorktreeId);
@ -466,7 +482,7 @@ pub enum Event {
pub struct Workspace { pub struct Workspace {
weak_self: WeakViewHandle<Self>, weak_self: WeakViewHandle<Self>,
remote_entity_subscription: Option<client::Subscription>, remote_entity_subscription: Option<client::Subscription>,
modal: Option<AnyViewHandle>, modal: Option<ActiveModal>,
zoomed: Option<AnyWeakViewHandle>, zoomed: Option<AnyWeakViewHandle>,
zoomed_position: Option<DockPosition>, zoomed_position: Option<DockPosition>,
center: PaneGroup, center: PaneGroup,
@ -495,6 +511,11 @@ pub struct Workspace {
pane_history_timestamp: Arc<AtomicUsize>, pane_history_timestamp: Arc<AtomicUsize>,
} }
struct ActiveModal {
view: Box<dyn ModalHandle>,
previously_focused_view_id: Option<usize>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct ViewId { pub struct ViewId {
pub creator: PeerId, pub creator: PeerId,
@ -1482,8 +1503,10 @@ impl Workspace {
cx.notify(); cx.notify();
// Whatever modal was visible is getting clobbered. If its the same type as V, then return // Whatever modal was visible is getting clobbered. If its the same type as V, then return
// it. Otherwise, create a new modal and set it as active. // it. Otherwise, create a new modal and set it as active.
let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>()); if let Some(already_open_modal) = self
if let Some(already_open_modal) = already_open_modal { .dismiss_modal(cx)
.and_then(|modal| modal.downcast::<V>())
{
cx.focus_self(); cx.focus_self();
Some(already_open_modal) Some(already_open_modal)
} else { } else {
@ -1494,8 +1517,12 @@ impl Workspace {
} }
}) })
.detach(); .detach();
let previously_focused_view_id = cx.focused_view_id();
cx.focus(&modal); cx.focus(&modal);
self.modal = Some(modal.into_any()); self.modal = Some(ActiveModal {
view: Box::new(modal),
previously_focused_view_id,
});
None None
} }
} }
@ -1503,13 +1530,20 @@ impl Workspace {
pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> { pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
self.modal self.modal
.as_ref() .as_ref()
.and_then(|modal| modal.clone().downcast::<V>()) .and_then(|modal| modal.view.as_any().clone().downcast::<V>())
} }
pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) { pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) -> Option<AnyViewHandle> {
if self.modal.take().is_some() { if let Some(modal) = self.modal.take() {
cx.focus(&self.active_pane); if let Some(previously_focused_view_id) = modal.previously_focused_view_id {
if modal.view.has_focus(cx) {
cx.window_context().focus(Some(previously_focused_view_id));
}
}
cx.notify(); cx.notify();
Some(modal.view.as_any().clone())
} else {
None
} }
} }
@ -3496,7 +3530,7 @@ impl View for Workspace {
) )
})) }))
.with_children(self.modal.as_ref().map(|modal| { .with_children(self.modal.as_ref().map(|modal| {
ChildView::new(modal, cx) ChildView::new(modal.view.as_any(), cx)
.contained() .contained()
.with_style(theme.workspace.modal) .with_style(theme.workspace.modal)
.aligned() .aligned()
@ -4775,6 +4809,7 @@ mod tests {
theme::init((), cx); theme::init((), cx);
language::init(cx); language::init(cx);
crate::init_settings(cx); crate::init_settings(cx);
Project::init_settings(cx);
}); });
} }
} }

View File

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor." description = "The fast, collaborative code editor."
edition = "2021" edition = "2021"
name = "zed" name = "zed"
version = "0.94.0" version = "0.95.0"
publish = false publish = false
[lib] [lib]
@ -16,6 +16,7 @@ name = "Zed"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
audio = { path = "../audio" }
activity_indicator = { path = "../activity_indicator" } activity_indicator = { path = "../activity_indicator" }
auto_update = { path = "../auto_update" } auto_update = { path = "../auto_update" }
breadcrumbs = { path = "../breadcrumbs" } breadcrumbs = { path = "../breadcrumbs" }

View File

@ -7,6 +7,7 @@ use rust_embed::RustEmbed;
#[include = "fonts/**/*"] #[include = "fonts/**/*"]
#[include = "icons/**/*"] #[include = "icons/**/*"]
#[include = "themes/**/*"] #[include = "themes/**/*"]
#[include = "sounds/**/*"]
#[include = "*.md"] #[include = "*.md"]
#[exclude = "*.DS_Store"] #[exclude = "*.DS_Store"]
pub struct Assets; pub struct Assets;

View File

@ -57,8 +57,9 @@ use staff_mode::StaffMode;
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt}; use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
use workspace::{item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace}; use workspace::{item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace};
use zed::{ use zed::{
assets::Assets, build_window_options, handle_keymap_file_changes, initialize_workspace, assets::Assets,
languages, menus, build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
only_instance::{ensure_only_instance, IsOnlyInstance},
}; };
fn main() { fn main() {
@ -66,6 +67,10 @@ fn main() {
init_paths(); init_paths();
init_logger(); init_logger();
if ensure_only_instance() != IsOnlyInstance::Yes {
return;
}
log::info!("========== starting zed =========="); log::info!("========== starting zed ==========");
let mut app = gpui::App::new(Assets).unwrap(); let mut app = gpui::App::new(Assets).unwrap();
@ -180,6 +185,8 @@ fn main() {
background_actions, background_actions,
}); });
cx.set_global(Arc::downgrade(&app_state)); cx.set_global(Arc::downgrade(&app_state));
audio::init(Assets, cx);
auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx); auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx);
workspace::init(app_state.clone(), cx); workspace::init(app_state.clone(), cx);

View File

@ -0,0 +1,103 @@
use std::{
io::{Read, Write},
net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener, TcpStream},
thread,
time::Duration,
};
use util::channel::ReleaseChannel;
const LOCALHOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
const CONNECT_TIMEOUT: Duration = Duration::from_millis(10);
const RECEIVE_TIMEOUT: Duration = Duration::from_millis(35);
const SEND_TIMEOUT: Duration = Duration::from_millis(20);
fn address() -> SocketAddr {
let port = match *util::channel::RELEASE_CHANNEL {
ReleaseChannel::Dev => 43737,
ReleaseChannel::Preview => 43738,
ReleaseChannel::Stable => 43739,
};
SocketAddr::V4(SocketAddrV4::new(LOCALHOST, port))
}
fn instance_handshake() -> &'static str {
match *util::channel::RELEASE_CHANNEL {
ReleaseChannel::Dev => "Zed Editor Dev Instance Running",
ReleaseChannel::Preview => "Zed Editor Preview Instance Running",
ReleaseChannel::Stable => "Zed Editor Stable Instance Running",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IsOnlyInstance {
Yes,
No,
}
pub fn ensure_only_instance() -> IsOnlyInstance {
if *db::ZED_STATELESS {
return IsOnlyInstance::Yes;
}
if check_got_handshake() {
return IsOnlyInstance::No;
}
let listener = match TcpListener::bind(address()) {
Ok(listener) => listener,
Err(err) => {
log::warn!("Error binding to single instance port: {err}");
if check_got_handshake() {
return IsOnlyInstance::No;
}
// Avoid failing to start when some other application by chance already has
// a claim on the port. This is sub-par as any other instance that gets launched
// will be unable to communicate with this instance and will duplicate
log::warn!("Backup handshake request failed, continuing without handshake");
return IsOnlyInstance::Yes;
}
};
thread::spawn(move || {
for stream in listener.incoming() {
let mut stream = match stream {
Ok(stream) => stream,
Err(_) => return,
};
_ = stream.set_nodelay(true);
_ = stream.set_read_timeout(Some(SEND_TIMEOUT));
_ = stream.write_all(instance_handshake().as_bytes());
}
});
IsOnlyInstance::Yes
}
fn check_got_handshake() -> bool {
match TcpStream::connect_timeout(&address(), CONNECT_TIMEOUT) {
Ok(mut stream) => {
let mut buf = vec![0u8; instance_handshake().len()];
stream.set_read_timeout(Some(RECEIVE_TIMEOUT)).unwrap();
if let Err(err) = stream.read_exact(&mut buf) {
log::warn!("Connected to single instance port but failed to read: {err}");
return false;
}
if buf == instance_handshake().as_bytes() {
log::info!("Got instance handshake");
return true;
}
log::warn!("Got wrong instance handshake value");
false
}
Err(_) => false,
}
}

View File

@ -1,6 +1,7 @@
pub mod assets; pub mod assets;
pub mod languages; pub mod languages;
pub mod menus; pub mod menus;
pub mod only_instance;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub mod test; pub mod test;
@ -2074,6 +2075,167 @@ mod tests {
line!(), line!(),
); );
#[track_caller]
fn assert_key_bindings_for<'a>(
window_id: usize,
cx: &TestAppContext,
actions: Vec<(&'static str, &'a dyn Action)>,
line: u32,
) {
for (key, action) in actions {
// assert that...
assert!(
cx.available_actions(window_id, 0)
.into_iter()
.any(|(_, bound_action, b)| {
// action names match...
bound_action.name() == action.name()
&& bound_action.namespace() == action.namespace()
// and key strokes contain the given key
&& b.iter()
.any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
}),
"On {} Failed to find {} with key binding {}",
line,
action.name(),
key
);
}
}
}
#[gpui::test]
async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
struct TestView;
impl Entity for TestView {
type Event = ();
}
impl View for TestView {
fn ui_name() -> &'static str {
"TestView"
}
fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
Empty::new().into_any()
}
}
let executor = cx.background();
let fs = FakeFs::new(executor.clone());
actions!(test, [A, B]);
// From the Atom keymap
actions!(workspace, [ActivatePreviousPane]);
// From the JetBrains keymap
actions!(pane, [ActivatePrevItem]);
fs.save(
"/settings.json".as_ref(),
&r#"
{
"base_keymap": "Atom"
}
"#
.into(),
Default::default(),
)
.await
.unwrap();
fs.save(
"/keymap.json".as_ref(),
&r#"
[
{
"bindings": {
"backspace": "test::A"
}
}
]
"#
.into(),
Default::default(),
)
.await
.unwrap();
cx.update(|cx| {
cx.set_global(SettingsStore::test(cx));
theme::init(Assets, cx);
welcome::init(cx);
cx.add_global_action(|_: &A, _cx| {});
cx.add_global_action(|_: &B, _cx| {});
cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
let settings_rx = watch_config_file(
executor.clone(),
fs.clone(),
PathBuf::from("/settings.json"),
);
let keymap_rx =
watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
handle_keymap_file_changes(keymap_rx, cx);
handle_settings_file_changes(settings_rx, cx);
});
cx.foreground().run_until_parked();
let (window_id, _view) = cx.add_window(|_| TestView);
// Test loading the keymap base at all
assert_key_bindings_for(
window_id,
cx,
vec![("backspace", &A), ("k", &ActivatePreviousPane)],
line!(),
);
// Test disabling the key binding for the base keymap
fs.save(
"/keymap.json".as_ref(),
&r#"
[
{
"bindings": {
"backspace": null
}
}
]
"#
.into(),
Default::default(),
)
.await
.unwrap();
cx.foreground().run_until_parked();
assert_key_bindings_for(window_id, cx, vec![("k", &ActivatePreviousPane)], line!());
// Test modifying the base, while retaining the users keymap
fs.save(
"/settings.json".as_ref(),
&r#"
{
"base_keymap": "JetBrains"
}
"#
.into(),
Default::default(),
)
.await
.unwrap();
cx.foreground().run_until_parked();
assert_key_bindings_for(window_id, cx, vec![("[", &ActivatePrevItem)], line!());
#[track_caller]
fn assert_key_bindings_for<'a>( fn assert_key_bindings_for<'a>(
window_id: usize, window_id: usize,
cx: &TestAppContext, cx: &TestAppContext,
@ -2160,6 +2322,7 @@ mod tests {
state.initialize_workspace = initialize_workspace; state.initialize_workspace = initialize_workspace;
state.build_window_options = build_window_options; state.build_window_options = build_window_options;
theme::init((), cx); theme::init((), cx);
audio::init((), cx);
call::init(app_state.client.clone(), app_state.user_store.clone(), cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
workspace::init(app_state.clone(), cx); workspace::init(app_state.clone(), cx);
Project::init_settings(cx); Project::init_settings(cx);

View File

@ -35,7 +35,7 @@ Match a property identifier and highlight it using the identifier `@property`. I
``` ```
```ts ```ts
function buildDefaultSyntax(colorScheme: ColorScheme): Partial<Syntax> { function buildDefaultSyntax(colorScheme: Theme): Partial<Syntax> {
// ... // ...
} }
``` ```

View File

@ -27,7 +27,8 @@
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.1.5", "typescript": "^5.1.5",
"utility-types": "^3.10.0", "utility-types": "^3.10.0",
"vitest": "^0.32.0" "vitest": "^0.32.0",
"zustand": "^4.3.8"
} }
}, },
"node_modules/@aashutoshrathi/word-wrap": { "node_modules/@aashutoshrathi/word-wrap": {
@ -2595,6 +2596,12 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"peer": true
},
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@ -2706,6 +2713,18 @@
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
}, },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"peer": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/loupe": { "node_modules/loupe": {
"version": "2.3.6", "version": "2.3.6",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz",
@ -3292,6 +3311,18 @@
} }
] ]
}, },
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "17.0.2", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@ -4025,6 +4056,14 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/utility-types": { "node_modules/utility-types": {
"version": "3.10.0", "version": "3.10.0",
"resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz",
@ -4305,6 +4344,29 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/zustand": {
"version": "4.3.8",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.8.tgz",
"integrity": "sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg==",
"dependencies": {
"use-sync-external-store": "1.2.0"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"immer": ">=9.0",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
} }
} }
} }

View File

@ -16,21 +16,22 @@
"@tokens-studio/types": "^0.2.3", "@tokens-studio/types": "^0.2.3",
"@types/chroma-js": "^2.4.0", "@types/chroma-js": "^2.4.0",
"@types/node": "^18.14.1", "@types/node": "^18.14.1",
"@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.60.1",
"@vitest/coverage-v8": "^0.32.0",
"ayu": "^8.0.1", "ayu": "^8.0.1",
"chroma-js": "^2.4.2", "chroma-js": "^2.4.2",
"deepmerge": "^4.3.0", "deepmerge": "^4.3.0",
"eslint": "^8.43.0",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-import": "^2.27.5",
"json-schema-to-typescript": "^13.0.2", "json-schema-to-typescript": "^13.0.2",
"toml": "^3.0.0", "toml": "^3.0.0",
"ts-deepmerge": "^6.0.3", "ts-deepmerge": "^6.0.3",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.1.5",
"utility-types": "^3.10.0", "utility-types": "^3.10.0",
"vitest": "^0.32.0", "vitest": "^0.32.0",
"@typescript-eslint/eslint-plugin": "^5.60.1", "zustand": "^4.3.8"
"@typescript-eslint/parser": "^5.60.1",
"@vitest/coverage-v8": "^0.32.0",
"eslint": "^8.43.0",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-import": "^2.27.5",
"typescript": "^5.1.5"
} }
} }

View File

@ -2,8 +2,9 @@ import * as fs from "fs"
import { tmpdir } from "os" import { tmpdir } from "os"
import * as path from "path" import * as path from "path"
import app from "./style_tree/app" import app from "./style_tree/app"
import { ColorScheme, create_color_scheme } from "./theme/color_scheme" import { Theme, create_theme } from "./theme/create_theme"
import { themes } from "./themes" import { themes } from "./themes"
import { useThemeStore } from "./theme"
const assets_directory = `${__dirname}/../../assets` const assets_directory = `${__dirname}/../../assets`
const temp_directory = fs.mkdtempSync(path.join(tmpdir(), "build-themes")) const temp_directory = fs.mkdtempSync(path.join(tmpdir(), "build-themes"))
@ -20,15 +21,22 @@ function clear_themes(theme_directory: string) {
} }
} }
function write_themes(themes: ColorScheme[], output_directory: string) { const all_themes: Theme[] = themes.map((theme) =>
create_theme(theme)
)
function write_themes(themes: Theme[], output_directory: string) {
clear_themes(output_directory) clear_themes(output_directory)
for (const color_scheme of themes) { for (const theme of themes) {
const style_tree = app(color_scheme) const { setTheme } = useThemeStore.getState()
setTheme(theme)
const style_tree = app()
const style_tree_json = JSON.stringify(style_tree, null, 2) const style_tree_json = JSON.stringify(style_tree, null, 2)
const temp_path = path.join(temp_directory, `${color_scheme.name}.json`) const temp_path = path.join(temp_directory, `${theme.name}.json`)
const out_path = path.join( const out_path = path.join(
output_directory, output_directory,
`${color_scheme.name}.json` `${theme.name}.json`
) )
fs.writeFileSync(temp_path, style_tree_json) fs.writeFileSync(temp_path, style_tree_json)
fs.renameSync(temp_path, out_path) fs.renameSync(temp_path, out_path)
@ -36,8 +44,4 @@ function write_themes(themes: ColorScheme[], output_directory: string) {
} }
} }
const all_themes: ColorScheme[] = themes.map((theme) =>
create_color_scheme(theme)
)
write_themes(all_themes, `${assets_directory}/themes`) write_themes(all_themes, `${assets_directory}/themes`)

View File

@ -1,9 +1,9 @@
import * as fs from "fs" import * as fs from "fs"
import * as path from "path" import * as path from "path"
import { ColorScheme, create_color_scheme } from "./common" import { Theme, create_theme, useThemeStore } from "./common"
import { themes } from "./themes" import { themes } from "./themes"
import { slugify } from "./utils/slugify" import { slugify } from "./utils/slugify"
import { theme_tokens } from "./theme/tokens/color_scheme" import { theme_tokens } from "./theme/tokens/theme"
const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens") const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens")
const TOKENS_FILE = path.join(TOKENS_DIRECTORY, "$themes.json") const TOKENS_FILE = path.join(TOKENS_DIRECTORY, "$themes.json")
@ -27,7 +27,7 @@ type TokenSet = {
selected_token_sets: { [key: string]: "enabled" } selected_token_sets: { [key: string]: "enabled" }
} }
function build_token_set_order(theme: ColorScheme[]): { function build_token_set_order(theme: Theme[]): {
token_set_order: string[] token_set_order: string[]
} { } {
const token_set_order: string[] = theme.map((scheme) => const token_set_order: string[] = theme.map((scheme) =>
@ -36,7 +36,7 @@ function build_token_set_order(theme: ColorScheme[]): {
return { token_set_order } return { token_set_order }
} }
function build_themes_index(theme: ColorScheme[]): TokenSet[] { function build_themes_index(theme: Theme[]): TokenSet[] {
const themes_index: TokenSet[] = theme.map((scheme, index) => { const themes_index: TokenSet[] = theme.map((scheme, index) => {
const id = `${scheme.is_light ? "light" : "dark"}_${scheme.name const id = `${scheme.is_light ? "light" : "dark"}_${scheme.name
.toLowerCase() .toLowerCase()
@ -55,12 +55,15 @@ function build_themes_index(theme: ColorScheme[]): TokenSet[] {
return themes_index return themes_index
} }
function write_tokens(themes: ColorScheme[], tokens_directory: string) { function write_tokens(themes: Theme[], tokens_directory: string) {
clear_tokens(tokens_directory) clear_tokens(tokens_directory)
for (const theme of themes) { for (const theme of themes) {
const { setTheme } = useThemeStore.getState()
setTheme(theme)
const file_name = slugify(theme.name) + ".json" const file_name = slugify(theme.name) + ".json"
const tokens = theme_tokens(theme) const tokens = theme_tokens()
const tokens_json = JSON.stringify(tokens, null, 2) const tokens_json = JSON.stringify(tokens, null, 2)
const out_path = path.join(tokens_directory, file_name) const out_path = path.join(tokens_directory, file_name)
fs.writeFileSync(out_path, tokens_json, { mode: 0o644 }) fs.writeFileSync(out_path, tokens_json, { mode: 0o644 })
@ -80,8 +83,8 @@ function write_tokens(themes: ColorScheme[], tokens_directory: string) {
console.log(`- ${METADATA_FILE} created`) console.log(`- ${METADATA_FILE} created`)
} }
const all_themes: ColorScheme[] = themes.map((theme) => const all_themes: Theme[] = themes.map((theme) =>
create_color_scheme(theme) create_theme(theme)
) )
write_tokens(all_themes, TOKENS_DIRECTORY) write_tokens(all_themes, TOKENS_DIRECTORY)

View File

@ -1,6 +1,6 @@
import { interactive, toggleable } from "../element" import { interactive, toggleable } from "../element"
import { background, foreground } from "../style_tree/components" import { background, foreground } from "../style_tree/components"
import { ColorScheme } from "../theme/color_scheme" import { useTheme, Theme } from "../theme"
export type Margin = { export type Margin = {
top: number top: number
@ -11,21 +11,20 @@ export type Margin = {
interface IconButtonOptions { interface IconButtonOptions {
layer?: layer?:
| ColorScheme["lowest"] | Theme["lowest"]
| ColorScheme["middle"] | Theme["middle"]
| ColorScheme["highest"] | Theme["highest"]
color?: keyof ColorScheme["lowest"] color?: keyof Theme["lowest"]
margin?: Partial<Margin> margin?: Partial<Margin>
} }
type ToggleableIconButtonOptions = IconButtonOptions & { type ToggleableIconButtonOptions = IconButtonOptions & {
active_color?: keyof ColorScheme["lowest"] active_color?: keyof Theme["lowest"]
} }
export function icon_button( export function icon_button({ color, margin, layer }: IconButtonOptions) {
theme: ColorScheme, const theme = useTheme()
{ color, margin, layer }: IconButtonOptions
) {
if (!color) color = "base" if (!color) color = "base"
const m = { const m = {
@ -68,15 +67,15 @@ export function icon_button(
} }
export function toggleable_icon_button( export function toggleable_icon_button(
theme: ColorScheme, theme: Theme,
{ color, active_color, margin }: ToggleableIconButtonOptions { color, active_color, margin }: ToggleableIconButtonOptions
) { ) {
if (!color) color = "base" if (!color) color = "base"
return toggleable({ return toggleable({
state: { state: {
inactive: icon_button(theme, { color, margin }), inactive: icon_button({ color, margin }),
active: icon_button(theme, { active: icon_button({
color: active_color ? active_color : color, color: active_color ? active_color : color,
margin, margin,
layer: theme.middle, layer: theme.middle,

View File

@ -5,27 +5,30 @@ import {
foreground, foreground,
text, text,
} from "../style_tree/components" } from "../style_tree/components"
import { ColorScheme } from "../theme/color_scheme" import { useTheme, Theme } from "../theme"
import { Margin } from "./icon_button" import { Margin } from "./icon_button"
interface TextButtonOptions { interface TextButtonOptions {
layer?: layer?:
| ColorScheme["lowest"] | Theme["lowest"]
| ColorScheme["middle"] | Theme["middle"]
| ColorScheme["highest"] | Theme["highest"]
color?: keyof ColorScheme["lowest"] color?: keyof Theme["lowest"]
margin?: Partial<Margin> margin?: Partial<Margin>
text_properties?: TextProperties text_properties?: TextProperties
} }
type ToggleableTextButtonOptions = TextButtonOptions & { type ToggleableTextButtonOptions = TextButtonOptions & {
active_color?: keyof ColorScheme["lowest"] active_color?: keyof Theme["lowest"]
} }
export function text_button( export function text_button({
theme: ColorScheme, color,
{ color, layer, margin, text_properties }: TextButtonOptions layer,
) { margin,
text_properties,
}: TextButtonOptions) {
const theme = useTheme()
if (!color) color = "base" if (!color) color = "base"
const text_options: TextProperties = { const text_options: TextProperties = {
@ -72,15 +75,15 @@ export function text_button(
} }
export function toggleable_text_button( export function toggleable_text_button(
theme: ColorScheme, theme: Theme,
{ color, active_color, margin }: ToggleableTextButtonOptions { color, active_color, margin }: ToggleableTextButtonOptions
) { ) {
if (!color) color = "base" if (!color) color = "base"
return toggleable({ return toggleable({
state: { state: {
inactive: text_button(theme, { color, margin }), inactive: text_button({ color, margin }),
active: text_button(theme, { active: text_button({
color: active_color ? active_color : color, color: active_color ? active_color : color,
margin, margin,
layer: theme.middle, layer: theme.middle,

Some files were not shown because too many files have changed in this diff Show More