Compare commits
1 Commits
tasks-moda
...
fix-node-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e3d25b1e0 |
191
Cargo.lock
generated
191
Cargo.lock
generated
@@ -348,9 +348,7 @@ dependencies = [
|
||||
"file_icons",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"gray_matter",
|
||||
"http 0.1.0",
|
||||
"indoc",
|
||||
"language",
|
||||
@@ -360,7 +358,6 @@ dependencies = [
|
||||
"open_ai",
|
||||
"ordered-float 2.10.0",
|
||||
"parking_lot",
|
||||
"picker",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
@@ -2827,7 +2824,7 @@ dependencies = [
|
||||
"cranelift-entity",
|
||||
"cranelift-isle",
|
||||
"gimli",
|
||||
"hashbrown 0.14.5",
|
||||
"hashbrown 0.14.0",
|
||||
"log",
|
||||
"regalloc2",
|
||||
"smallvec",
|
||||
@@ -3147,7 +3144,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"hashbrown 0.14.5",
|
||||
"hashbrown 0.14.0",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
@@ -3639,12 +3636,11 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "erased-serde"
|
||||
version = "0.4.5"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d"
|
||||
checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"typeid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3790,7 +3786,6 @@ dependencies = [
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
"project",
|
||||
"release_channel",
|
||||
"schemars",
|
||||
"semantic_version",
|
||||
"serde",
|
||||
@@ -3845,7 +3840,6 @@ dependencies = [
|
||||
"language",
|
||||
"picker",
|
||||
"project",
|
||||
"release_channel",
|
||||
"semantic_version",
|
||||
"serde",
|
||||
"settings",
|
||||
@@ -3979,7 +3973,6 @@ dependencies = [
|
||||
"menu",
|
||||
"picker",
|
||||
"project",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"text",
|
||||
@@ -4527,7 +4520,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
|
||||
dependencies = [
|
||||
"fallible-iterator",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.0.0",
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
@@ -4795,18 +4788,6 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gray_matter"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7188a951c53316d94711b3d944c28cf79968685d295cbe782494e8811fc75554"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"toml 0.5.11",
|
||||
"yaml-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grid"
|
||||
version = "0.13.0"
|
||||
@@ -4873,9 +4854,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
|
||||
dependencies = [
|
||||
"ahash 0.8.8",
|
||||
"allocator-api2",
|
||||
@@ -4887,7 +4868,7 @@ version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
"hashbrown 0.14.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5305,12 +5286,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.2.6"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
||||
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.14.5",
|
||||
"hashbrown 0.14.0",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -5540,9 +5521,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
@@ -5944,12 +5925,6 @@ dependencies = [
|
||||
"safemem",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "linkify"
|
||||
version = "0.10.0"
|
||||
@@ -6053,9 +6028,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.21"
|
||||
version = "0.4.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"value-bag",
|
||||
@@ -6415,7 +6390,7 @@ dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"codespan-reporting",
|
||||
"hexf-parse",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.0.0",
|
||||
"log",
|
||||
"num-traits",
|
||||
"rustc-hash",
|
||||
@@ -6845,8 +6820,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"hashbrown 0.14.5",
|
||||
"indexmap 2.2.6",
|
||||
"hashbrown 0.14.0",
|
||||
"indexmap 2.0.0",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
@@ -7278,7 +7253,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7707,12 +7682,10 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"sha2 0.10.7",
|
||||
"shlex",
|
||||
"similar",
|
||||
"smol",
|
||||
"snippet",
|
||||
"task",
|
||||
"tempfile",
|
||||
"terminal",
|
||||
"text",
|
||||
"unindent",
|
||||
@@ -8067,7 +8040,6 @@ name = "recent_projects"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"dev_server_projects",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
@@ -8083,8 +8055,6 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"task",
|
||||
"terminal_view",
|
||||
"ui",
|
||||
"ui_text_field",
|
||||
"util",
|
||||
@@ -8657,9 +8627,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
|
||||
|
||||
[[package]]
|
||||
name = "safemem"
|
||||
@@ -8988,18 +8958,18 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.202"
|
||||
version = "1.0.196"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
|
||||
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.202"
|
||||
version = "1.0.196"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
|
||||
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -9028,11 +8998,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.117"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
|
||||
checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.0.0",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
@@ -9044,7 +9014,7 @@ version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26386958a1344003f2b2bcff51a23fbe70461a478ef29247c6c6ab2c1656f53e"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.0.0",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
@@ -9560,7 +9530,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"hashlink",
|
||||
"hex",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.0.0",
|
||||
"log",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
@@ -10153,7 +10123,6 @@ dependencies = [
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"parking_lot",
|
||||
"picker",
|
||||
"project",
|
||||
"schemars",
|
||||
@@ -10161,7 +10130,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"task",
|
||||
"text",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript",
|
||||
"ui",
|
||||
@@ -10677,7 +10645,7 @@ version = "0.19.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.0.0",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
@@ -10690,7 +10658,7 @@ version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.0.0",
|
||||
"toml_datetime",
|
||||
"winnow 0.5.15",
|
||||
]
|
||||
@@ -10701,7 +10669,7 @@ version = "0.22.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.0.0",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
@@ -11125,12 +11093,6 @@ dependencies = [
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "059d83cc991e7a42fc37bd50941885db0888e34209f8cfd9aab07ddec03bc9cf"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
@@ -11367,9 +11329,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "value-bag"
|
||||
version = "1.9.0"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101"
|
||||
checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3"
|
||||
dependencies = [
|
||||
"value-bag-serde1",
|
||||
"value-bag-sval2",
|
||||
@@ -11377,9 +11339,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "value-bag-serde1"
|
||||
version = "1.9.0"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccacf50c5cb077a9abb723c5bcb5e0754c1a433f1e1de89edc328e2760b6328b"
|
||||
checksum = "b0b9f3feef403a50d4d67e9741a6d8fc688bcbb4e4f31bd4aab72cc690284394"
|
||||
dependencies = [
|
||||
"erased-serde",
|
||||
"serde",
|
||||
@@ -11388,9 +11350,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "value-bag-sval2"
|
||||
version = "1.9.0"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1785bae486022dfb9703915d42287dcb284c1ee37bd1080eeba78cc04721285b"
|
||||
checksum = "30b24f4146b6f3361e91cbf527d1fb35e9376c3c0cef72ca5ec5af6d640fad7d"
|
||||
dependencies = [
|
||||
"sval",
|
||||
"sval_buffer",
|
||||
@@ -11641,7 +11603,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fd83062c17b9f4985d438603cde0a5e8c5c8198201a6937f778b607924c7da2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.0.0",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@@ -11657,7 +11619,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.0.0",
|
||||
"semver",
|
||||
]
|
||||
|
||||
@@ -11684,7 +11646,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"encoding_rs",
|
||||
"gimli",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.0.0",
|
||||
"libc",
|
||||
"log",
|
||||
"object",
|
||||
@@ -11815,7 +11777,7 @@ dependencies = [
|
||||
"cpp_demangle",
|
||||
"cranelift-entity",
|
||||
"gimli",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.0.0",
|
||||
"log",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
@@ -11866,7 +11828,7 @@ dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"encoding_rs",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.0.0",
|
||||
"libc",
|
||||
"log",
|
||||
"mach",
|
||||
@@ -11971,7 +11933,7 @@ checksum = "96326c9800fb6c099f50d1bd2126d636fc2f96950e1675acf358c0f52516cd38"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck 0.4.1",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.0.0",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
@@ -12666,7 +12628,7 @@ checksum = "d8a39a15d1ae2077688213611209849cad40e9e5cccf6e61951a425850677ff3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck 0.4.1",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.0.0",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
@@ -12694,7 +12656,7 @@ checksum = "421c0c848a0660a8c22e2fd217929a0191f14476b68962afd2af89fd22e39825"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.4.2",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.0.0",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -12713,7 +12675,7 @@ checksum = "196d3ecfc4b759a8573bf86a9b3f8996b304b3732e4c7de81655f875f6efdca6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.0.0",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
@@ -12880,7 +12842,7 @@ version = "0.4.0"
|
||||
source = "git+https://github.com/npmania/xim-rs?rev=27132caffc5b9bc9c432ca4afad184ab6e7c16af#27132caffc5b9bc9c432ca4afad184ab6e7c16af"
|
||||
dependencies = [
|
||||
"ahash 0.8.8",
|
||||
"hashbrown 0.14.5",
|
||||
"hashbrown 0.14.0",
|
||||
"log",
|
||||
"x11rb",
|
||||
"xim-ctext",
|
||||
@@ -12943,15 +12905,6 @@ dependencies = [
|
||||
"toml 0.8.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
||||
dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "0.5.1"
|
||||
@@ -13042,7 +12995,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.138.0"
|
||||
version = "0.137.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -13165,28 +13118,28 @@ dependencies = [
|
||||
name = "zed_dart"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_deno"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_elixir"
|
||||
version = "0.0.4"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_elm"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13215,8 +13168,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "zed_extension_api"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ca8bcaea3feb2d2ce9dbeb061ee48365312a351faa7014c417b0365fe9e459"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -13225,7 +13176,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_extension_api"
|
||||
version = "0.0.7"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ca8bcaea3feb2d2ce9dbeb061ee48365312a351faa7014c417b0365fe9e459"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -13236,47 +13189,47 @@ dependencies = [
|
||||
name = "zed_gleam"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_glsl"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_haskell"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_html"
|
||||
version = "0.1.1"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_lua"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_ocaml"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_php"
|
||||
version = "0.0.4"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.4",
|
||||
]
|
||||
@@ -13297,30 +13250,30 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_ruby"
|
||||
version = "0.0.4"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_svelte"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_terraform"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_toml"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13334,14 +13287,14 @@ dependencies = [
|
||||
name = "zed_vue"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_zig"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.7",
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -330,7 +330,6 @@ serde_json_lenient = { version = "0.1", features = [
|
||||
serde_repr = "0.1"
|
||||
sha2 = "0.10"
|
||||
shellexpand = "2.1.0"
|
||||
shlex = "1.3.0"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2"
|
||||
strum = { version = "0.25.0", features = ["derive"] }
|
||||
@@ -376,7 +375,7 @@ unindent = "0.1.7"
|
||||
unicase = "2.6"
|
||||
unicode-segmentation = "1.10"
|
||||
url = "2.2"
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] }
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5"] }
|
||||
wasmparser = "0.201"
|
||||
wasm-encoder = "0.201"
|
||||
wasmtime = { version = "19.0.0", default-features = false, features = [
|
||||
|
||||
@@ -501,12 +501,6 @@
|
||||
"tab": "editor::ConfirmCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_completion && !showing_completions",
|
||||
"bindings": {
|
||||
"tab": "editor::AcceptInlineCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_code_actions",
|
||||
"bindings": {
|
||||
|
||||
@@ -515,12 +515,6 @@
|
||||
"tab": "editor::ConfirmCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_completion && !showing_completions",
|
||||
"bindings": {
|
||||
"tab": "editor::AcceptInlineCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_code_actions",
|
||||
"bindings": {
|
||||
|
||||
@@ -493,8 +493,8 @@
|
||||
"shift-o": "vim::OtherEnd",
|
||||
"d": "vim::VisualDelete",
|
||||
"x": "vim::VisualDelete",
|
||||
"shift-d": "vim::VisualDeleteLine",
|
||||
"shift-x": "vim::VisualDeleteLine",
|
||||
"shift-d": "vim::VisualDelete",
|
||||
"shift-x": "vim::VisualDelete",
|
||||
"y": "vim::VisualYank",
|
||||
"shift-y": "vim::VisualYank",
|
||||
"p": "vim::Paste",
|
||||
|
||||
@@ -316,7 +316,7 @@
|
||||
// AI provider.
|
||||
"provider": {
|
||||
"name": "openai",
|
||||
// The default model to use when creating new contexts. This
|
||||
// The default model to use when starting new conversations. This
|
||||
// setting can take three values:
|
||||
//
|
||||
// 1. "gpt-3.5-turbo"
|
||||
@@ -836,7 +836,7 @@
|
||||
// environment variables.
|
||||
//
|
||||
// Examples:
|
||||
// - "proxy": "socks5://localhost:10808"
|
||||
// - "proxy": "http://127.0.0.1:10809"
|
||||
// - "proxy" = "socks5://localhost:10808"
|
||||
// - "proxy" = "http://127.0.0.1:10809"
|
||||
"proxy": null
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ editor.workspace = true
|
||||
file_icons.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
http.workspace = true
|
||||
indoc.workspace = true
|
||||
@@ -50,8 +49,6 @@ ui.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
picker.workspace = true
|
||||
gray_matter = "0.2.7"
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
|
||||
@@ -34,12 +34,10 @@ impl Default for CurrentProjectContext {
|
||||
impl CurrentProjectContext {
|
||||
/// Returns the [`CurrentProjectContext`] as a message to the language model.
|
||||
pub fn to_message(&self) -> Option<LanguageModelRequestMessage> {
|
||||
self.enabled
|
||||
.then(|| LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: self.message.clone(),
|
||||
})
|
||||
.filter(|message| !message.content.is_empty())
|
||||
self.enabled.then(|| LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: self.message.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Updates the [`CurrentProjectContext`] for the given [`Project`].
|
||||
|
||||
@@ -87,12 +87,10 @@ impl RecentBuffersContext {
|
||||
|
||||
/// Returns the [`RecentBuffersContext`] as a message to the language model.
|
||||
pub fn to_message(&self) -> Option<LanguageModelRequestMessage> {
|
||||
self.enabled
|
||||
.then(|| LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: self.snapshot.message.to_string(),
|
||||
})
|
||||
.filter(|message| !message.content.is_empty())
|
||||
self.enabled.then(|| LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: self.snapshot.message.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,10 @@ pub mod assistant_panel;
|
||||
pub mod assistant_settings;
|
||||
mod codegen;
|
||||
mod completion_provider;
|
||||
mod omit_ranges;
|
||||
mod prompt_library;
|
||||
mod prompts;
|
||||
mod saved_conversation;
|
||||
mod search;
|
||||
mod slash_command;
|
||||
mod streaming_diff;
|
||||
|
||||
use ambient_context::AmbientContextSnapshot;
|
||||
@@ -17,7 +16,6 @@ use client::{proto, Client};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
pub(crate) use completion_provider::*;
|
||||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||
pub(crate) use prompts::prompt_library::*;
|
||||
pub(crate) use saved_conversation::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -339,11 +339,11 @@ pub struct LegacyAssistantSettingsContent {
|
||||
///
|
||||
/// Default: 320
|
||||
pub default_height: Option<f32>,
|
||||
/// The default OpenAI model to use when creating new contexts.
|
||||
/// The default OpenAI model to use when starting new conversations.
|
||||
///
|
||||
/// Default: gpt-4-1106-preview
|
||||
pub default_open_ai_model: Option<OpenAiModel>,
|
||||
/// OpenAI API base URL to use when creating new contexts.
|
||||
/// OpenAI API base URL to use when starting new conversations.
|
||||
///
|
||||
/// Default: https://api.openai.com/v1
|
||||
pub openai_api_url: Option<String>,
|
||||
|
||||
@@ -233,7 +233,7 @@ impl CompletionProvider {
|
||||
CompletionProvider::Anthropic(provider) => provider.count_tokens(request, cx),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.count_tokens(request, cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => futures::FutureExt::boxed(futures::future::ready(Ok(0))),
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
use rope::Rope;
|
||||
use std::{cmp::Ordering, ops::Range};
|
||||
|
||||
pub(crate) fn text_in_range_omitting_ranges(
|
||||
rope: &Rope,
|
||||
range: Range<usize>,
|
||||
omit_ranges: &[Range<usize>],
|
||||
) -> String {
|
||||
let mut content = String::with_capacity(range.len());
|
||||
let mut omit_ranges = omit_ranges
|
||||
.iter()
|
||||
.skip_while(|omit_range| omit_range.end <= range.start)
|
||||
.peekable();
|
||||
let mut offset = range.start;
|
||||
let mut chunks = rope.chunks_in_range(range.clone());
|
||||
while let Some(chunk) = chunks.next() {
|
||||
if let Some(omit_range) = omit_ranges.peek() {
|
||||
match offset.cmp(&omit_range.start) {
|
||||
Ordering::Less => {
|
||||
let max_len = omit_range.start - offset;
|
||||
if chunk.len() < max_len {
|
||||
content.push_str(chunk);
|
||||
offset += chunk.len();
|
||||
} else {
|
||||
content.push_str(&chunk[..max_len]);
|
||||
chunks.seek(omit_range.end.min(range.end));
|
||||
offset = omit_range.end;
|
||||
omit_ranges.next();
|
||||
}
|
||||
}
|
||||
Ordering::Equal | Ordering::Greater => {
|
||||
chunks.seek(omit_range.end.min(range.end));
|
||||
offset = omit_range.end;
|
||||
omit_ranges.next();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
content.push_str(chunk);
|
||||
offset += chunk.len();
|
||||
}
|
||||
}
|
||||
|
||||
content
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::{rngs::StdRng, Rng as _};
|
||||
use util::RandomCharIter;
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_text_in_range_omitting_ranges(mut rng: StdRng) {
|
||||
let text = RandomCharIter::new(&mut rng).take(1024).collect::<String>();
|
||||
let rope = Rope::from(text.as_str());
|
||||
|
||||
let mut start = rng.gen_range(0..=text.len() / 2);
|
||||
let mut end = rng.gen_range(text.len() / 2..=text.len());
|
||||
while !text.is_char_boundary(start) {
|
||||
start -= 1;
|
||||
}
|
||||
while !text.is_char_boundary(end) {
|
||||
end += 1;
|
||||
}
|
||||
let range = start..end;
|
||||
|
||||
let mut ix = 0;
|
||||
let mut omit_ranges = Vec::new();
|
||||
for _ in 0..rng.gen_range(0..10) {
|
||||
let mut start = rng.gen_range(ix..=text.len());
|
||||
while !text.is_char_boundary(start) {
|
||||
start += 1;
|
||||
}
|
||||
let mut end = rng.gen_range(start..=text.len());
|
||||
while !text.is_char_boundary(end) {
|
||||
end += 1;
|
||||
}
|
||||
omit_ranges.push(start..end);
|
||||
ix = end;
|
||||
if ix == text.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut expected_text = text[range.clone()].to_string();
|
||||
for omit_range in omit_ranges.iter().rev() {
|
||||
let start = omit_range
|
||||
.start
|
||||
.saturating_sub(range.start)
|
||||
.min(range.len());
|
||||
let end = omit_range.end.saturating_sub(range.start).min(range.len());
|
||||
expected_text.replace_range(start..end, "");
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
text_in_range_omitting_ranges(&rope, range.clone(), &omit_ranges),
|
||||
expected_text,
|
||||
"text: {text:?}\nrange: {range:?}\nomit_ranges: {omit_ranges:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
454
crates/assistant/src/prompt_library.rs
Normal file
454
crates/assistant/src/prompt_library.rs
Normal file
@@ -0,0 +1,454 @@
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::{AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Render};
|
||||
use parking_lot::RwLock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, Checkbox, ModalHeader};
|
||||
use util::{paths::PROMPTS_DIR, ResultExt};
|
||||
use workspace::ModalView;
|
||||
|
||||
pub struct PromptLibraryState {
|
||||
/// The default prompt all assistant contexts will start with
|
||||
_system_prompt: String,
|
||||
/// All [UserPrompt]s loaded into the library
|
||||
prompts: HashMap<String, UserPrompt>,
|
||||
/// Prompts included in the default prompt
|
||||
default_prompts: Vec<String>,
|
||||
/// Prompts that have a pending update that hasn't been applied yet
|
||||
_updateable_prompts: Vec<String>,
|
||||
/// Prompts that have been changed since they were loaded
|
||||
/// and can be reverted to their original state
|
||||
_revertable_prompts: Vec<String>,
|
||||
version: usize,
|
||||
}
|
||||
|
||||
pub struct PromptLibrary {
|
||||
state: RwLock<PromptLibraryState>,
|
||||
}
|
||||
|
||||
impl Default for PromptLibrary {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PromptLibrary {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
state: RwLock::new(PromptLibraryState {
|
||||
_system_prompt: String::new(),
|
||||
prompts: HashMap::new(),
|
||||
default_prompts: Vec::new(),
|
||||
_updateable_prompts: Vec::new(),
|
||||
_revertable_prompts: Vec::new(),
|
||||
version: 0,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn init(fs: Arc<dyn Fs>) -> anyhow::Result<Self> {
|
||||
let prompt_library = PromptLibrary::new();
|
||||
prompt_library.load_prompts(fs)?;
|
||||
Ok(prompt_library)
|
||||
}
|
||||
|
||||
fn load_prompts(&self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
|
||||
let prompts = futures::executor::block_on(UserPrompt::list(fs))?;
|
||||
let prompts_with_ids = prompts
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|prompt| {
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
(id, prompt)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut state = self.state.write();
|
||||
state.prompts.extend(prompts_with_ids);
|
||||
state.version += 1;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn default_prompt(&self) -> Option<String> {
|
||||
let state = self.state.read();
|
||||
|
||||
if state.default_prompts.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.join_default_prompts())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_prompt_to_default(&self, prompt_id: String) -> anyhow::Result<()> {
|
||||
let mut state = self.state.write();
|
||||
|
||||
if !state.default_prompts.contains(&prompt_id) && state.prompts.contains_key(&prompt_id) {
|
||||
state.default_prompts.push(prompt_id);
|
||||
state.version += 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_prompt_from_default(&self, prompt_id: String) -> anyhow::Result<()> {
|
||||
let mut state = self.state.write();
|
||||
|
||||
state.default_prompts.retain(|id| id != &prompt_id);
|
||||
state.version += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn join_default_prompts(&self) -> String {
|
||||
let state = self.state.read();
|
||||
let active_prompt_ids = state.default_prompts.to_vec();
|
||||
|
||||
active_prompt_ids
|
||||
.iter()
|
||||
.filter_map(|id| state.prompts.get(id).map(|p| p.prompt.clone()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n---\n\n")
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn prompts(&self) -> Vec<UserPrompt> {
|
||||
let state = self.state.read();
|
||||
state.prompts.values().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn prompts_with_ids(&self) -> Vec<(String, UserPrompt)> {
|
||||
let state = self.state.read();
|
||||
state
|
||||
.prompts
|
||||
.iter()
|
||||
.map(|(id, prompt)| (id.clone(), prompt.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn _default_prompts(&self) -> Vec<UserPrompt> {
|
||||
let state = self.state.read();
|
||||
state
|
||||
.default_prompts
|
||||
.iter()
|
||||
.filter_map(|id| state.prompts.get(id).cloned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn default_prompt_ids(&self) -> Vec<String> {
|
||||
let state = self.state.read();
|
||||
state.default_prompts.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// A custom prompt that can be loaded into the prompt library
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// ```json
|
||||
/// {
|
||||
/// "title": "Foo",
|
||||
/// "version": "1.0",
|
||||
/// "author": "Jane Kim <jane@kim.com>",
|
||||
/// "languages": ["*"], // or ["rust", "python", "javascript"] etc...
|
||||
/// "prompt": "bar"
|
||||
/// }
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct UserPrompt {
|
||||
version: String,
|
||||
title: String,
|
||||
author: String,
|
||||
languages: Vec<String>,
|
||||
prompt: String,
|
||||
}
|
||||
|
||||
impl UserPrompt {
|
||||
async fn list(fs: Arc<dyn Fs>) -> anyhow::Result<Vec<Self>> {
|
||||
fs.create_dir(&PROMPTS_DIR).await?;
|
||||
|
||||
let mut paths = fs.read_dir(&PROMPTS_DIR).await?;
|
||||
let mut prompts = Vec::new();
|
||||
|
||||
while let Some(path_result) = paths.next().await {
|
||||
let path = match path_result {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("Error reading path: {:?}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if path.extension() == Some(std::ffi::OsStr::new("json")) {
|
||||
match fs.load(&path).await {
|
||||
Ok(content) => {
|
||||
let user_prompt: UserPrompt =
|
||||
serde_json::from_str(&content).map_err(|e| {
|
||||
anyhow::anyhow!("Failed to deserialize UserPrompt: {}", e)
|
||||
})?;
|
||||
|
||||
prompts.push(user_prompt);
|
||||
}
|
||||
Err(e) => eprintln!("Failed to load file {}: {}", path.display(), e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(prompts)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PromptManager {
|
||||
focus_handle: FocusHandle,
|
||||
prompt_library: Arc<PromptLibrary>,
|
||||
active_prompt: Option<String>,
|
||||
}
|
||||
|
||||
impl PromptManager {
|
||||
pub fn new(prompt_library: Arc<PromptLibrary>, cx: &mut WindowContext) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
Self {
|
||||
focus_handle,
|
||||
prompt_library,
|
||||
active_prompt: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_active_prompt(&mut self, prompt_id: Option<String>) {
|
||||
self.active_prompt = prompt_id;
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for PromptManager {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let prompt_library = self.prompt_library.clone();
|
||||
let prompts = prompt_library
|
||||
.clone()
|
||||
.prompts_with_ids()
|
||||
.clone()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let active_prompt = self.active_prompt.as_ref().and_then(|id| {
|
||||
prompt_library
|
||||
.prompts_with_ids()
|
||||
.iter()
|
||||
.find(|(prompt_id, _)| prompt_id == id)
|
||||
.map(|(_, prompt)| prompt.clone())
|
||||
});
|
||||
|
||||
v_flex()
|
||||
.key_context("PromptManager")
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::dismiss))
|
||||
.elevation_3(cx)
|
||||
.size_full()
|
||||
.flex_none()
|
||||
.w(rems(54.))
|
||||
.h(rems(40.))
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
ModalHeader::new("prompt-manager-header")
|
||||
.child(Headline::new("Prompt Library").size(HeadlineSize::Small))
|
||||
.show_dismiss_button(true),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_grow()
|
||||
.overflow_hidden()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
div()
|
||||
.id("prompt-preview")
|
||||
.overflow_y_scroll()
|
||||
.h_full()
|
||||
.min_w_64()
|
||||
.max_w_1_2()
|
||||
.child(
|
||||
v_flex()
|
||||
.justify_start()
|
||||
.py(Spacing::Medium.rems(cx))
|
||||
.px(Spacing::Large.rems(cx))
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.when_else(
|
||||
!prompts.is_empty(),
|
||||
|with_items| {
|
||||
with_items.children(prompts.into_iter().map(
|
||||
|(id, prompt)| {
|
||||
let prompt_library = prompt_library.clone();
|
||||
let prompt = prompt.clone();
|
||||
let prompt_id = id.clone();
|
||||
let shared_string_id: SharedString =
|
||||
id.clone().into();
|
||||
|
||||
let default_prompt_ids =
|
||||
prompt_library.clone().default_prompt_ids();
|
||||
let is_default =
|
||||
default_prompt_ids.contains(&id);
|
||||
// We'll use this for conditionally enabled prompts
|
||||
// like those loaded only for certain languages
|
||||
let is_conditional = false;
|
||||
let selection =
|
||||
match (is_default, is_conditional) {
|
||||
(_, true) => Selection::Indeterminate,
|
||||
(true, _) => Selection::Selected,
|
||||
(false, _) => Selection::Unselected,
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.id(ElementId::Name(
|
||||
format!("prompt-{}", shared_string_id)
|
||||
.into(),
|
||||
))
|
||||
.p(Spacing::Small.rems(cx))
|
||||
|
||||
.on_click(cx.listener({
|
||||
let prompt_id = prompt_id.clone();
|
||||
move |this, _event, _cx| {
|
||||
this.set_active_prompt(Some(
|
||||
prompt_id.clone(),
|
||||
));
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap(Spacing::Large.rems(cx))
|
||||
.child(
|
||||
Checkbox::new(
|
||||
shared_string_id,
|
||||
selection,
|
||||
)
|
||||
.on_click(move |_, _cx| {
|
||||
if is_default {
|
||||
prompt_library
|
||||
.clone()
|
||||
.remove_prompt_from_default(
|
||||
prompt_id.clone(),
|
||||
)
|
||||
.log_err();
|
||||
} else {
|
||||
prompt_library
|
||||
.clone()
|
||||
.add_prompt_to_default(
|
||||
prompt_id.clone(),
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(Label::new(
|
||||
prompt.title,
|
||||
)),
|
||||
)
|
||||
.child(div()),
|
||||
)
|
||||
},
|
||||
))
|
||||
},
|
||||
|no_items| {
|
||||
no_items.child(
|
||||
Label::new("No prompts").color(Color::Placeholder),
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("prompt-preview")
|
||||
.overflow_y_scroll()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.size_full()
|
||||
.flex_none()
|
||||
.child(
|
||||
v_flex()
|
||||
.justify_start()
|
||||
.py(Spacing::Medium.rems(cx))
|
||||
.px(Spacing::Large.rems(cx))
|
||||
.gap(Spacing::Large.rems(cx))
|
||||
.when_else(
|
||||
active_prompt.is_some(),
|
||||
|with_prompt| {
|
||||
let active_prompt = active_prompt.as_ref().unwrap();
|
||||
with_prompt
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Headline::new(
|
||||
active_prompt.title.clone(),
|
||||
)
|
||||
.size(HeadlineSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.child(
|
||||
Label::new(
|
||||
active_prompt
|
||||
.author
|
||||
.clone(),
|
||||
)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(
|
||||
if active_prompt
|
||||
.languages
|
||||
.is_empty()
|
||||
|| active_prompt
|
||||
.languages[0]
|
||||
== "*"
|
||||
{
|
||||
" · Global".to_string()
|
||||
} else {
|
||||
format!(
|
||||
" · {}",
|
||||
active_prompt
|
||||
.languages
|
||||
.join(", ")
|
||||
)
|
||||
},
|
||||
)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.max_w(rems(30.))
|
||||
.text_ui(cx)
|
||||
.child(active_prompt.prompt.clone()),
|
||||
)
|
||||
},
|
||||
|without_prompt| {
|
||||
without_prompt.justify_center().items_center().child(
|
||||
Label::new("Select a prompt to view details.")
|
||||
.color(Color::Placeholder),
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for PromptManager {}
|
||||
impl ModalView for PromptManager {}
|
||||
|
||||
impl FocusableView for PromptManager {
|
||||
fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,95 @@
|
||||
pub mod prompt;
|
||||
pub mod prompt_library;
|
||||
pub mod prompt_manager;
|
||||
use language::BufferSnapshot;
|
||||
use std::{fmt::Write, ops::Range};
|
||||
|
||||
pub fn generate_content_prompt(
|
||||
user_prompt: String,
|
||||
language_name: Option<&str>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
project_name: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
let mut prompt = String::new();
|
||||
|
||||
let content_type = match language_name {
|
||||
None | Some("Markdown" | "Plain Text") => {
|
||||
writeln!(prompt, "You are an expert engineer.")?;
|
||||
"Text"
|
||||
}
|
||||
Some(language_name) => {
|
||||
writeln!(prompt, "You are an expert {language_name} engineer.")?;
|
||||
writeln!(
|
||||
prompt,
|
||||
"Your answer MUST always and only be valid {}.",
|
||||
language_name
|
||||
)?;
|
||||
"Code"
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(project_name) = project_name {
|
||||
writeln!(
|
||||
prompt,
|
||||
"You are currently working inside the '{project_name}' project in code editor Zed."
|
||||
)?;
|
||||
}
|
||||
|
||||
// Include file content.
|
||||
for chunk in buffer.text_for_range(0..range.start) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
if range.is_empty() {
|
||||
prompt.push_str("<|START|>");
|
||||
} else {
|
||||
prompt.push_str("<|START|");
|
||||
}
|
||||
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
if !range.is_empty() {
|
||||
prompt.push_str("|END|>");
|
||||
}
|
||||
|
||||
for chunk in buffer.text_for_range(range.end..buffer.len()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
prompt.push('\n');
|
||||
|
||||
if range.is_empty() {
|
||||
writeln!(
|
||||
prompt,
|
||||
"Assume the cursor is located where the `<|START|>` span is."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"{content_type} can't be replaced, so assume your answer will be inserted at the cursor.",
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Generate {content_type} based on the users prompt: {user_prompt}",
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap();
|
||||
writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Double check that you only return code and not the '<|START|' and '|END|'> spans"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(prompt, "Never make remarks about the output.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Do not return anything else, except the generated {content_type}."
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Ok(prompt)
|
||||
}
|
||||
|
||||
@@ -1,278 +0,0 @@
|
||||
use language::BufferSnapshot;
|
||||
use std::{fmt::Write, ops::Range};
|
||||
use ui::SharedString;
|
||||
|
||||
use gray_matter::{engine::YAML, Matter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct StaticPromptFrontmatter {
|
||||
title: String,
|
||||
version: String,
|
||||
author: String,
|
||||
#[serde(default)]
|
||||
languages: Vec<String>,
|
||||
#[serde(default)]
|
||||
dependencies: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for StaticPromptFrontmatter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
title: "New Prompt".to_string(),
|
||||
version: "1.0".to_string(),
|
||||
author: "No Author".to_string(),
|
||||
languages: vec!["*".to_string()],
|
||||
dependencies: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticPromptFrontmatter {
|
||||
pub fn title(&self) -> SharedString {
|
||||
self.title.clone().into()
|
||||
}
|
||||
|
||||
// pub fn version(&self) -> SharedString {
|
||||
// self.version.clone().into()
|
||||
// }
|
||||
|
||||
// pub fn author(&self) -> SharedString {
|
||||
// self.author.clone().into()
|
||||
// }
|
||||
|
||||
// pub fn languages(&self) -> Vec<SharedString> {
|
||||
// self.languages
|
||||
// .clone()
|
||||
// .into_iter()
|
||||
// .map(|s| s.into())
|
||||
// .collect()
|
||||
// }
|
||||
|
||||
// pub fn dependencies(&self) -> Vec<SharedString> {
|
||||
// self.dependencies
|
||||
// .clone()
|
||||
// .into_iter()
|
||||
// .map(|s| s.into())
|
||||
// .collect()
|
||||
// }
|
||||
}
|
||||
|
||||
/// A statuc prompt that can be loaded into the prompt library
|
||||
/// from Markdown with a frontmatter header
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// ### Globally available prompt
|
||||
///
|
||||
/// ```markdown
|
||||
/// ---
|
||||
/// title: Foo
|
||||
/// version: 1.0
|
||||
/// author: Jane Kim <jane@kim.com
|
||||
/// languages: ["*"]
|
||||
/// dependencies: []
|
||||
/// ---
|
||||
///
|
||||
/// Foo and bar are terms used in programming to describe generic concepts.
|
||||
/// ```
|
||||
///
|
||||
/// ### Language-specific prompt
|
||||
///
|
||||
/// ```markdown
|
||||
/// ---
|
||||
/// title: UI with GPUI
|
||||
/// version: 1.0
|
||||
/// author: Nate Butler <iamnbutler@gmail.com>
|
||||
/// languages: ["rust"]
|
||||
/// dependencies: ["gpui"]
|
||||
/// ---
|
||||
///
|
||||
/// When building a UI with GPUI, ensure you...
|
||||
/// ```
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct StaticPrompt {
|
||||
content: String,
|
||||
file_name: Option<String>,
|
||||
}
|
||||
|
||||
impl StaticPrompt {
|
||||
pub fn new(content: String) -> Self {
|
||||
StaticPrompt {
|
||||
content,
|
||||
file_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn title(&self) -> Option<SharedString> {
|
||||
self.metadata().map(|m| m.title())
|
||||
}
|
||||
|
||||
// pub fn version(&self) -> Option<SharedString> {
|
||||
// self.metadata().map(|m| m.version())
|
||||
// }
|
||||
|
||||
// pub fn author(&self) -> Option<SharedString> {
|
||||
// self.metadata().map(|m| m.author())
|
||||
// }
|
||||
|
||||
// pub fn languages(&self) -> Vec<SharedString> {
|
||||
// self.metadata().map(|m| m.languages()).unwrap_or_default()
|
||||
// }
|
||||
|
||||
// pub fn dependencies(&self) -> Vec<SharedString> {
|
||||
// self.metadata()
|
||||
// .map(|m| m.dependencies())
|
||||
// .unwrap_or_default()
|
||||
// }
|
||||
|
||||
// pub fn load(fs: Arc<Fs>, file_name: String) -> anyhow::Result<Self> {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// pub fn save(&self, fs: Arc<Fs>) -> anyhow::Result<()> {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// pub fn rename(&self, new_file_name: String, fs: Arc<Fs>) -> anyhow::Result<()> {
|
||||
// todo!()
|
||||
// }
|
||||
}
|
||||
|
||||
impl StaticPrompt {
|
||||
// pub fn update(&mut self, contents: String) -> &mut Self {
|
||||
// self.content = contents;
|
||||
// self
|
||||
// }
|
||||
|
||||
/// Sets the file name of the prompt
|
||||
pub fn file_name(&mut self, file_name: String) -> &mut Self {
|
||||
self.file_name = Some(file_name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the file name of the prompt based on the title
|
||||
// pub fn file_name_from_title(&mut self) -> &mut Self {
|
||||
// if let Some(title) = self.title() {
|
||||
// let file_name = title.to_lowercase().replace(" ", "_");
|
||||
// if !file_name.is_empty() {
|
||||
// self.file_name = Some(file_name);
|
||||
// }
|
||||
// }
|
||||
// self
|
||||
// }
|
||||
|
||||
/// Returns the prompt's content
|
||||
pub fn content(&self) -> &String {
|
||||
&self.content
|
||||
}
|
||||
fn parse(&self) -> anyhow::Result<(StaticPromptFrontmatter, String)> {
|
||||
let matter = Matter::<YAML>::new();
|
||||
let result = matter.parse(self.content.as_str());
|
||||
match result.data {
|
||||
Some(data) => {
|
||||
let front_matter: StaticPromptFrontmatter = data.deserialize()?;
|
||||
let body = result.content;
|
||||
Ok((front_matter, body))
|
||||
}
|
||||
None => Err(anyhow::anyhow!("Failed to parse frontmatter")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn metadata(&self) -> Option<StaticPromptFrontmatter> {
|
||||
self.parse().ok().map(|(front_matter, _)| front_matter)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_content_prompt(
|
||||
user_prompt: String,
|
||||
language_name: Option<&str>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
project_name: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
let mut prompt = String::new();
|
||||
|
||||
let content_type = match language_name {
|
||||
None | Some("Markdown" | "Plain Text") => {
|
||||
writeln!(prompt, "You are an expert engineer.")?;
|
||||
"Text"
|
||||
}
|
||||
Some(language_name) => {
|
||||
writeln!(prompt, "You are an expert {language_name} engineer.")?;
|
||||
writeln!(
|
||||
prompt,
|
||||
"Your answer MUST always and only be valid {}.",
|
||||
language_name
|
||||
)?;
|
||||
"Code"
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(project_name) = project_name {
|
||||
writeln!(
|
||||
prompt,
|
||||
"You are currently working inside the '{project_name}' project in code editor Zed."
|
||||
)?;
|
||||
}
|
||||
|
||||
// Include file content.
|
||||
for chunk in buffer.text_for_range(0..range.start) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
if range.is_empty() {
|
||||
prompt.push_str("<|START|>");
|
||||
} else {
|
||||
prompt.push_str("<|START|");
|
||||
}
|
||||
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
if !range.is_empty() {
|
||||
prompt.push_str("|END|>");
|
||||
}
|
||||
|
||||
for chunk in buffer.text_for_range(range.end..buffer.len()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
prompt.push('\n');
|
||||
|
||||
if range.is_empty() {
|
||||
writeln!(
|
||||
prompt,
|
||||
"Assume the cursor is located where the `<|START|>` span is."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"{content_type} can't be replaced, so assume your answer will be inserted at the cursor.",
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Generate {content_type} based on the users prompt: {user_prompt}",
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap();
|
||||
writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Double check that you only return code and not the '<|START|' and '|END|'> spans"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(prompt, "Never make remarks about the output.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Do not return anything else, except the generated {content_type}."
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Ok(prompt)
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
use anyhow::Context;
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
|
||||
use parking_lot::RwLock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smol::stream::StreamExt;
|
||||
use std::sync::Arc;
|
||||
use util::paths::PROMPTS_DIR;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::prompt::StaticPrompt;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct PromptId(pub Uuid);
|
||||
|
||||
#[allow(unused)]
|
||||
impl PromptId {
|
||||
pub fn new() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct PromptLibraryState {
|
||||
/// A set of prompts that all assistant contexts will start with
|
||||
default_prompt: Vec<PromptId>,
|
||||
/// All [Prompt]s loaded into the library
|
||||
prompts: HashMap<PromptId, StaticPrompt>,
|
||||
/// Prompts that have been changed but haven't been
|
||||
/// saved back to the file system
|
||||
dirty_prompts: Vec<PromptId>,
|
||||
version: usize,
|
||||
}
|
||||
|
||||
pub struct PromptLibrary {
|
||||
state: RwLock<PromptLibraryState>,
|
||||
}
|
||||
|
||||
impl Default for PromptLibrary {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PromptLibrary {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
state: RwLock::new(PromptLibraryState::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prompts(&self) -> Vec<(PromptId, StaticPrompt)> {
|
||||
let state = self.state.read();
|
||||
state
|
||||
.prompts
|
||||
.iter()
|
||||
.map(|(id, prompt)| (*id, prompt.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn first_prompt_id(&self) -> Option<PromptId> {
|
||||
let state = self.state.read();
|
||||
state.prompts.keys().next().cloned()
|
||||
}
|
||||
|
||||
pub fn prompt(&self, id: PromptId) -> Option<StaticPrompt> {
|
||||
let state = self.state.read();
|
||||
state.prompts.get(&id).cloned()
|
||||
}
|
||||
|
||||
/// Save the current state of the prompt library to the
|
||||
/// file system as a JSON file
|
||||
pub async fn save(&self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
|
||||
fs.create_dir(&PROMPTS_DIR).await?;
|
||||
|
||||
let path = PROMPTS_DIR.join("index.json");
|
||||
|
||||
let json = {
|
||||
let state = self.state.read();
|
||||
serde_json::to_string(&*state)?
|
||||
};
|
||||
|
||||
fs.atomic_write(path, json).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load the state of the prompt library from the file system
|
||||
/// or create a new one if it doesn't exist
|
||||
pub async fn load(fs: Arc<dyn Fs>) -> anyhow::Result<Self> {
|
||||
let path = PROMPTS_DIR.join("index.json");
|
||||
|
||||
let state = if fs.is_file(&path).await {
|
||||
let json = fs.load(&path).await?;
|
||||
serde_json::from_str(&json)?
|
||||
} else {
|
||||
PromptLibraryState::default()
|
||||
};
|
||||
|
||||
let mut prompt_library = Self {
|
||||
state: RwLock::new(state),
|
||||
};
|
||||
|
||||
prompt_library.load_prompts(fs).await?;
|
||||
|
||||
Ok(prompt_library)
|
||||
}
|
||||
|
||||
/// Load all prompts from the file system
|
||||
/// adding them to the library if they don't already exist
|
||||
pub async fn load_prompts(&mut self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
|
||||
// let current_prompts = self.all_prompt_contents().clone();
|
||||
|
||||
// For now, we'll just clear the prompts and reload them all
|
||||
self.state.get_mut().prompts.clear();
|
||||
|
||||
let mut prompt_paths = fs.read_dir(&PROMPTS_DIR).await?;
|
||||
|
||||
while let Some(prompt_path) = prompt_paths.next().await {
|
||||
let prompt_path = prompt_path.with_context(|| "Failed to read prompt path")?;
|
||||
|
||||
if !fs.is_file(&prompt_path).await
|
||||
|| prompt_path.extension().and_then(|ext| ext.to_str()) != Some("md")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let json = fs
|
||||
.load(&prompt_path)
|
||||
.await
|
||||
.with_context(|| format!("Failed to load prompt {:?}", prompt_path))?;
|
||||
let mut static_prompt = StaticPrompt::new(json);
|
||||
|
||||
if let Some(file_name) = prompt_path.file_name() {
|
||||
let file_name = file_name.to_string_lossy().into_owned();
|
||||
static_prompt.file_name(file_name);
|
||||
}
|
||||
|
||||
let state = self.state.get_mut();
|
||||
|
||||
let id = Uuid::new_v4();
|
||||
state.prompts.insert(PromptId(id), static_prompt);
|
||||
state.version += 1;
|
||||
}
|
||||
|
||||
// Write any changes back to the file system
|
||||
self.save(fs.clone()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,327 +0,0 @@
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use fs::Fs;
|
||||
use gpui::{prelude::FluentBuilder, *};
|
||||
use language::{language_settings, Buffer, LanguageRegistry};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::ModalView;
|
||||
|
||||
use super::prompt_library::{PromptId, PromptLibrary};
|
||||
use crate::prompts::prompt::StaticPrompt;
|
||||
|
||||
pub struct PromptManager {
|
||||
focus_handle: FocusHandle,
|
||||
prompt_library: Arc<PromptLibrary>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
#[allow(dead_code)]
|
||||
fs: Arc<dyn Fs>,
|
||||
picker: View<Picker<PromptManagerDelegate>>,
|
||||
prompt_editors: HashMap<PromptId, View<Editor>>,
|
||||
active_prompt_id: Option<PromptId>,
|
||||
}
|
||||
|
||||
impl PromptManager {
|
||||
pub fn new(
|
||||
prompt_library: Arc<PromptLibrary>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let prompt_manager = cx.view().downgrade();
|
||||
let picker = cx.new_view(|cx| {
|
||||
Picker::uniform_list(
|
||||
PromptManagerDelegate {
|
||||
prompt_manager,
|
||||
matching_prompts: vec![],
|
||||
matching_prompt_ids: vec![],
|
||||
prompt_library: prompt_library.clone(),
|
||||
selected_index: 0,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.max_height(rems(35.75))
|
||||
.modal(false)
|
||||
});
|
||||
|
||||
let focus_handle = picker.focus_handle(cx);
|
||||
|
||||
let mut manager = Self {
|
||||
focus_handle,
|
||||
prompt_library,
|
||||
language_registry,
|
||||
fs,
|
||||
picker,
|
||||
prompt_editors: HashMap::default(),
|
||||
active_prompt_id: None,
|
||||
};
|
||||
|
||||
manager.active_prompt_id = manager.prompt_library.first_prompt_id();
|
||||
|
||||
manager
|
||||
}
|
||||
|
||||
pub fn set_active_prompt(&mut self, prompt_id: Option<PromptId>, cx: &mut ViewContext<Self>) {
|
||||
self.active_prompt_id = prompt_id;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn focus_active_editor(&self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(active_prompt_id) = self.active_prompt_id {
|
||||
if let Some(editor) = self.prompt_editors.get(&active_prompt_id) {
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
|
||||
cx.focus(&focus_handle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn render_prompt_list(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let picker = self.picker.clone();
|
||||
|
||||
v_flex()
|
||||
.id("prompt-list")
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.h_full()
|
||||
.w_2_5()
|
||||
.child(
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().background)
|
||||
.p(Spacing::Small.rems(cx))
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.h(rems(1.75))
|
||||
.w_full()
|
||||
.flex_none()
|
||||
.justify_between()
|
||||
.child(Label::new("Prompt Library").size(LabelSize::Small))
|
||||
.child(IconButton::new("new-prompt", IconName::Plus).disabled(true)),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.h(rems(38.25))
|
||||
.flex_grow()
|
||||
.justify_start()
|
||||
.child(picker),
|
||||
)
|
||||
}
|
||||
|
||||
fn set_editor_for_prompt(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let prompt_library = self.prompt_library.clone();
|
||||
|
||||
let editor_for_prompt = self.prompt_editors.entry(prompt_id).or_insert_with(|| {
|
||||
cx.new_view(|cx| {
|
||||
let text = if let Some(prompt) = prompt_library.prompt(prompt_id) {
|
||||
prompt.content().to_owned()
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let buffer = cx.new_model(|cx| {
|
||||
let mut buffer = Buffer::local(text, cx);
|
||||
let markdown = self.language_registry.language_for_name("Markdown");
|
||||
cx.spawn(|buffer, mut cx| async move {
|
||||
if let Some(markdown) = markdown.await.log_err() {
|
||||
_ = buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_language(Some(markdown), cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
buffer.set_language_registry(self.language_registry.clone());
|
||||
buffer
|
||||
});
|
||||
let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor
|
||||
})
|
||||
});
|
||||
editor_for_prompt.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for PromptManager {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
.key_context("PromptManager")
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::dismiss))
|
||||
// .on_action(cx.listener(Self::save_active_prompt))
|
||||
.elevation_3(cx)
|
||||
.size_full()
|
||||
.flex_none()
|
||||
.w(rems(64.))
|
||||
.h(rems(40.))
|
||||
.overflow_hidden()
|
||||
.child(self.render_prompt_list(cx))
|
||||
.child(
|
||||
div().w_3_5().h_full().child(
|
||||
v_flex()
|
||||
.id("prompt-editor")
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.size_full()
|
||||
.flex_none()
|
||||
.min_w_64()
|
||||
.h_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().background)
|
||||
.p(Spacing::Small.rems(cx))
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.h_7()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(div())
|
||||
.child(
|
||||
IconButton::new("dismiss", IconName::Close)
|
||||
.shape(IconButtonShape::Square)
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(menu::Cancel.boxed_clone());
|
||||
}),
|
||||
),
|
||||
)
|
||||
.when_some(self.active_prompt_id, |this, active_prompt_id| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.py(Spacing::Large.rems(cx))
|
||||
.px(Spacing::XLarge.rems(cx))
|
||||
.child(self.set_editor_for_prompt(active_prompt_id, cx)),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for PromptManager {}
|
||||
impl ModalView for PromptManager {}
|
||||
|
||||
impl FocusableView for PromptManager {
|
||||
fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PromptManagerDelegate {
|
||||
prompt_manager: WeakView<PromptManager>,
|
||||
matching_prompts: Vec<Arc<StaticPrompt>>,
|
||||
matching_prompt_ids: Vec<PromptId>,
|
||||
prompt_library: Arc<PromptLibrary>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl PickerDelegate for PromptManagerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
"Find a prompt…".into()
|
||||
}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matching_prompt_ids.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 selected_index_changed(
|
||||
&self,
|
||||
ix: usize,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Box<dyn Fn(&mut WindowContext) + 'static>> {
|
||||
let prompt_id = self.matching_prompt_ids.get(ix).copied()?;
|
||||
let prompt_manager = self.prompt_manager.upgrade()?;
|
||||
|
||||
Some(Box::new(move |cx| {
|
||||
prompt_manager.update(cx, |manager, cx| {
|
||||
manager.set_active_prompt(Some(prompt_id), cx);
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
let prompt_library = self.prompt_library.clone();
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
async {
|
||||
let prompts = prompt_library.prompts();
|
||||
let matching_prompts = prompts
|
||||
.into_iter()
|
||||
.filter(|(_, prompt)| {
|
||||
prompt
|
||||
.content()
|
||||
.to_lowercase()
|
||||
.contains(&query.to_lowercase())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
picker.delegate.matching_prompt_ids =
|
||||
matching_prompts.iter().map(|(id, _)| *id).collect();
|
||||
picker.delegate.matching_prompts = matching_prompts
|
||||
.into_iter()
|
||||
.map(|(_, prompt)| Arc::new(prompt))
|
||||
.collect();
|
||||
cx.notify();
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
.await;
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let prompt_manager = self.prompt_manager.upgrade().unwrap();
|
||||
prompt_manager.update(cx, move |manager, cx| manager.focus_active_editor(cx));
|
||||
}
|
||||
|
||||
fn should_dismiss(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.prompt_manager
|
||||
.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let matching_prompt = self.matching_prompts.get(ix)?;
|
||||
let prompt = matching_prompt.clone();
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.selected(selected)
|
||||
.child(Label::new(prompt.title().unwrap_or_default().clone())),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,319 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use editor::{CompletionProvider, Editor};
|
||||
use futures::channel::oneshot;
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{AppContext, Model, Task, ViewContext, WindowHandle};
|
||||
use language::{Anchor, Buffer, CodeLabel, Documentation, LanguageServerId, ToPoint};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use project::Project;
|
||||
use rope::Point;
|
||||
use std::{
|
||||
ops::Range,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::PromptLibrary;
|
||||
|
||||
mod current_file_command;
|
||||
mod file_command;
|
||||
mod prompt_command;
|
||||
|
||||
pub(crate) struct SlashCommandCompletionProvider {
|
||||
commands: Arc<SlashCommandRegistry>,
|
||||
cancel_flag: Mutex<Arc<AtomicBool>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct SlashCommandRegistry {
|
||||
commands: HashMap<String, Box<dyn SlashCommand>>,
|
||||
}
|
||||
|
||||
pub(crate) trait SlashCommand: 'static + Send + Sync {
|
||||
fn name(&self) -> String;
|
||||
fn description(&self) -> String;
|
||||
fn complete_argument(
|
||||
&self,
|
||||
query: String,
|
||||
cancel: Arc<AtomicBool>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>>;
|
||||
fn requires_argument(&self) -> bool;
|
||||
fn run(&self, argument: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation;
|
||||
}
|
||||
|
||||
pub(crate) struct SlashCommandInvocation {
|
||||
pub output: Task<Result<String>>,
|
||||
pub invalidated: oneshot::Receiver<()>,
|
||||
pub cleanup: SlashCommandCleanup,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct SlashCommandCleanup(Option<Box<dyn FnOnce()>>);
|
||||
|
||||
impl SlashCommandCleanup {
|
||||
pub fn new(cleanup: impl FnOnce() + 'static) -> Self {
|
||||
Self(Some(Box::new(cleanup)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SlashCommandCleanup {
|
||||
fn drop(&mut self) {
|
||||
if let Some(cleanup) = self.0.take() {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SlashCommandLine {
|
||||
/// The range within the line containing the command name.
|
||||
pub name: Range<usize>,
|
||||
/// The range within the line containing the command argument.
|
||||
pub argument: Option<Range<usize>>,
|
||||
}
|
||||
|
||||
impl SlashCommandRegistry {
|
||||
pub fn new(
|
||||
project: Model<Project>,
|
||||
prompt_library: Arc<PromptLibrary>,
|
||||
window: Option<WindowHandle<Workspace>>,
|
||||
) -> Arc<Self> {
|
||||
let mut this = Self {
|
||||
commands: HashMap::default(),
|
||||
};
|
||||
|
||||
this.register_command(file_command::FileSlashCommand::new(project));
|
||||
this.register_command(prompt_command::PromptSlashCommand::new(prompt_library));
|
||||
if let Some(window) = window {
|
||||
this.register_command(current_file_command::CurrentFileSlashCommand::new(window));
|
||||
}
|
||||
|
||||
Arc::new(this)
|
||||
}
|
||||
|
||||
fn register_command(&mut self, command: impl SlashCommand) {
|
||||
self.commands.insert(command.name(), Box::new(command));
|
||||
}
|
||||
|
||||
fn command_names(&self) -> impl Iterator<Item = &String> {
|
||||
self.commands.keys()
|
||||
}
|
||||
|
||||
pub(crate) fn command(&self, name: &str) -> Option<&dyn SlashCommand> {
|
||||
self.commands.get(name).map(|b| &**b)
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommandCompletionProvider {
|
||||
pub fn new(commands: Arc<SlashCommandRegistry>) -> Self {
|
||||
Self {
|
||||
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
|
||||
commands,
|
||||
}
|
||||
}
|
||||
|
||||
fn complete_command_name(
|
||||
&self,
|
||||
command_name: &str,
|
||||
range: Range<Anchor>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
let candidates = self
|
||||
.commands
|
||||
.command_names()
|
||||
.enumerate()
|
||||
.map(|(ix, def)| StringMatchCandidate {
|
||||
id: ix,
|
||||
string: def.clone(),
|
||||
char_bag: def.as_str().into(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let commands = self.commands.clone();
|
||||
let command_name = command_name.to_string();
|
||||
let executor = cx.background_executor().clone();
|
||||
executor.clone().spawn(async move {
|
||||
let matches = match_strings(
|
||||
&candidates,
|
||||
&command_name,
|
||||
true,
|
||||
usize::MAX,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(matches
|
||||
.into_iter()
|
||||
.filter_map(|mat| {
|
||||
let command = commands.command(&mat.string)?;
|
||||
let mut new_text = mat.string.clone();
|
||||
if command.requires_argument() {
|
||||
new_text.push(' ');
|
||||
}
|
||||
|
||||
Some(project::Completion {
|
||||
old_range: range.clone(),
|
||||
documentation: Some(Documentation::SingleLine(command.description())),
|
||||
new_text,
|
||||
label: CodeLabel::plain(mat.string, None),
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
fn complete_command_argument(
|
||||
&self,
|
||||
command_name: &str,
|
||||
argument: String,
|
||||
range: Range<Anchor>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
let new_cancel_flag = Arc::new(AtomicBool::new(false));
|
||||
let mut flag = self.cancel_flag.lock();
|
||||
flag.store(true, SeqCst);
|
||||
*flag = new_cancel_flag.clone();
|
||||
|
||||
if let Some(command) = self.commands.command(command_name) {
|
||||
let completions = command.complete_argument(argument, new_cancel_flag.clone(), cx);
|
||||
cx.background_executor().spawn(async move {
|
||||
Ok(completions
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|arg| project::Completion {
|
||||
old_range: range.clone(),
|
||||
label: CodeLabel::plain(arg.clone(), None),
|
||||
new_text: arg.clone(),
|
||||
documentation: None,
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
} else {
|
||||
cx.background_executor()
|
||||
.spawn(async move { Ok(Vec::new()) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
fn completions(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
buffer_position: Anchor,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
let task = buffer.update(cx, |buffer, cx| {
|
||||
let position = buffer_position.to_point(buffer);
|
||||
let line_start = Point::new(position.row, 0);
|
||||
let mut lines = buffer.text_for_range(line_start..position).lines();
|
||||
let line = lines.next()?;
|
||||
let call = SlashCommandLine::parse(line)?;
|
||||
|
||||
let name = &line[call.name.clone()];
|
||||
if let Some(argument) = call.argument {
|
||||
let start = buffer.anchor_after(Point::new(position.row, argument.start as u32));
|
||||
let argument = line[argument.clone()].to_string();
|
||||
Some(self.complete_command_argument(name, argument, start..buffer_position, cx))
|
||||
} else {
|
||||
let start = buffer.anchor_after(Point::new(position.row, call.name.start as u32));
|
||||
Some(self.complete_command_name(name, start..buffer_position, cx))
|
||||
}
|
||||
});
|
||||
|
||||
task.unwrap_or_else(|| Task::ready(Ok(Vec::new())))
|
||||
}
|
||||
|
||||
fn resolve_completions(
|
||||
&self,
|
||||
_: Model<Buffer>,
|
||||
_: Vec<usize>,
|
||||
_: Arc<RwLock<Box<[project::Completion]>>>,
|
||||
_: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<bool>> {
|
||||
Task::ready(Ok(true))
|
||||
}
|
||||
|
||||
fn apply_additional_edits_for_completion(
|
||||
&self,
|
||||
_: Model<Buffer>,
|
||||
_: project::Completion,
|
||||
_: bool,
|
||||
_: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<Option<language::Transaction>>> {
|
||||
Task::ready(Ok(None))
|
||||
}
|
||||
|
||||
fn is_completion_trigger(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
position: language::Anchor,
|
||||
_text: &str,
|
||||
_trigger_in_words: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> bool {
|
||||
let buffer = buffer.read(cx);
|
||||
let position = position.to_point(buffer);
|
||||
let line_start = Point::new(position.row, 0);
|
||||
let mut lines = buffer.text_for_range(line_start..position).lines();
|
||||
if let Some(line) = lines.next() {
|
||||
SlashCommandLine::parse(line).is_some()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommandLine {
|
||||
pub(crate) fn parse(line: &str) -> Option<Self> {
|
||||
let mut call: Option<Self> = None;
|
||||
let mut ix = 0;
|
||||
for c in line.chars() {
|
||||
let next_ix = ix + c.len_utf8();
|
||||
if let Some(call) = &mut call {
|
||||
// The command arguments start at the first non-whitespace character
|
||||
// after the command name, and continue until the end of the line.
|
||||
if let Some(argument) = &mut call.argument {
|
||||
if (*argument).is_empty() && c.is_whitespace() {
|
||||
argument.start = next_ix;
|
||||
}
|
||||
argument.end = next_ix;
|
||||
}
|
||||
// The command name ends at the first whitespace character.
|
||||
else if !call.name.is_empty() {
|
||||
if c.is_whitespace() {
|
||||
call.argument = Some(next_ix..next_ix);
|
||||
} else {
|
||||
call.name.end = next_ix;
|
||||
}
|
||||
}
|
||||
// The command name must begin with a letter.
|
||||
else if c.is_alphabetic() {
|
||||
call.name.end = next_ix;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
// Commands start with a slash.
|
||||
else if c == '/' {
|
||||
call = Some(SlashCommandLine {
|
||||
name: next_ix..next_ix,
|
||||
argument: None,
|
||||
});
|
||||
}
|
||||
// The line can't contain anything before the slash except for whitespace.
|
||||
else if !c.is_whitespace() {
|
||||
return None;
|
||||
}
|
||||
ix = next_ix;
|
||||
}
|
||||
call
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
use std::{borrow::Cow, cell::Cell, rc::Rc};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{AppContext, Entity, Subscription, Task, WindowHandle};
|
||||
use workspace::{Event as WorkspaceEvent, Workspace};
|
||||
|
||||
use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
|
||||
|
||||
pub(crate) struct CurrentFileSlashCommand {
|
||||
workspace: WindowHandle<Workspace>,
|
||||
}
|
||||
|
||||
impl CurrentFileSlashCommand {
|
||||
pub fn new(workspace: WindowHandle<Workspace>) -> Self {
|
||||
Self { workspace }
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommand for CurrentFileSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"current_file".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert the current file".into()
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn run(&self, _argument: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation {
|
||||
let (invalidate_tx, invalidate_rx) = oneshot::channel();
|
||||
let invalidate_tx = Rc::new(Cell::new(Some(invalidate_tx)));
|
||||
let mut subscriptions: Vec<Subscription> = Vec::new();
|
||||
let output = self.workspace.update(cx, |workspace, cx| {
|
||||
let mut timestamps_by_entity_id = HashMap::default();
|
||||
for pane in workspace.panes() {
|
||||
let pane = pane.read(cx);
|
||||
for entry in pane.activation_history() {
|
||||
timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
let mut most_recent_buffer = None;
|
||||
for editor in workspace.items_of_type::<Editor>(cx) {
|
||||
let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let timestamp = timestamps_by_entity_id
|
||||
.get(&editor.entity_id())
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
if most_recent_buffer
|
||||
.as_ref()
|
||||
.map_or(true, |(_, prev_timestamp)| timestamp > *prev_timestamp)
|
||||
{
|
||||
most_recent_buffer = Some((buffer, timestamp));
|
||||
}
|
||||
}
|
||||
|
||||
subscriptions.push({
|
||||
let workspace_view = cx.view().clone();
|
||||
let invalidate_tx = invalidate_tx.clone();
|
||||
cx.window_context()
|
||||
.subscribe(&workspace_view, move |_workspace, event, _cx| match event {
|
||||
WorkspaceEvent::ActiveItemChanged
|
||||
| WorkspaceEvent::ItemAdded
|
||||
| WorkspaceEvent::ItemRemoved
|
||||
| WorkspaceEvent::PaneAdded(_)
|
||||
| WorkspaceEvent::PaneRemoved => {
|
||||
if let Some(invalidate_tx) = invalidate_tx.take() {
|
||||
_ = invalidate_tx.send(());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
});
|
||||
|
||||
if let Some((buffer, _)) = most_recent_buffer {
|
||||
subscriptions.push({
|
||||
let invalidate_tx = invalidate_tx.clone();
|
||||
cx.window_context().observe(&buffer, move |_buffer, _cx| {
|
||||
if let Some(invalidate_tx) = invalidate_tx.take() {
|
||||
_ = invalidate_tx.send(());
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let path = snapshot.resolve_file_path(cx, true);
|
||||
cx.background_executor().spawn(async move {
|
||||
let path = path
|
||||
.as_ref()
|
||||
.map(|path| path.to_string_lossy())
|
||||
.unwrap_or_else(|| Cow::Borrowed("untitled"));
|
||||
|
||||
let mut output = String::with_capacity(path.len() + snapshot.len() + 9);
|
||||
output.push_str("```");
|
||||
output.push_str(&path);
|
||||
output.push('\n');
|
||||
for chunk in snapshot.as_rope().chunks() {
|
||||
output.push_str(chunk);
|
||||
}
|
||||
if !output.ends_with('\n') {
|
||||
output.push('\n');
|
||||
}
|
||||
output.push_str("```");
|
||||
Ok(output)
|
||||
})
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("no recent buffer found")))
|
||||
}
|
||||
});
|
||||
|
||||
SlashCommandInvocation {
|
||||
output: output.unwrap_or_else(|error| Task::ready(Err(error))),
|
||||
invalidated: invalidate_rx,
|
||||
cleanup: SlashCommandCleanup::new(move || drop(subscriptions)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
|
||||
use anyhow::Result;
|
||||
use futures::channel::oneshot;
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{AppContext, Model, Task};
|
||||
use project::{PathMatchCandidateSet, Project};
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
|
||||
pub(crate) struct FileSlashCommand {
|
||||
project: Model<Project>,
|
||||
}
|
||||
|
||||
impl FileSlashCommand {
|
||||
pub fn new(project: Model<Project>) -> Self {
|
||||
Self { project }
|
||||
}
|
||||
|
||||
fn search_paths(
|
||||
&self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Vec<PathMatch>> {
|
||||
let worktrees = self
|
||||
.project
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.collect::<Vec<_>>();
|
||||
let include_root_name = worktrees.len() > 1;
|
||||
let candidate_sets = worktrees
|
||||
.into_iter()
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
PathMatchCandidateSet {
|
||||
snapshot: worktree.snapshot(),
|
||||
include_ignored: worktree
|
||||
.root_entry()
|
||||
.map_or(false, |entry| entry.is_ignored),
|
||||
include_root_name,
|
||||
directories_only: false,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
fuzzy::match_path_sets(
|
||||
candidate_sets.as_slice(),
|
||||
query.as_str(),
|
||||
None,
|
||||
false,
|
||||
100,
|
||||
&cancellation_flag,
|
||||
executor,
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommand for FileSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"file".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert an entire file".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
cx: &mut AppContext,
|
||||
) -> gpui::Task<Result<Vec<String>>> {
|
||||
let paths = self.search_paths(query, cancellation_flag, cx);
|
||||
cx.background_executor().spawn(async move {
|
||||
Ok(paths
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|path_match| {
|
||||
format!(
|
||||
"{}{}",
|
||||
path_match.path_prefix,
|
||||
path_match.path.to_string_lossy()
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
fn run(&self, argument: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation {
|
||||
let project = self.project.read(cx);
|
||||
let Some(argument) = argument else {
|
||||
return SlashCommandInvocation {
|
||||
output: Task::ready(Err(anyhow::anyhow!("missing path"))),
|
||||
invalidated: oneshot::channel().1,
|
||||
cleanup: SlashCommandCleanup::default(),
|
||||
};
|
||||
};
|
||||
|
||||
let path = Path::new(argument);
|
||||
let abs_path = project.worktrees().find_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
worktree.entry_for_path(path)?;
|
||||
worktree.absolutize(path).ok()
|
||||
});
|
||||
|
||||
let Some(abs_path) = abs_path else {
|
||||
return SlashCommandInvocation {
|
||||
output: Task::ready(Err(anyhow::anyhow!("missing path"))),
|
||||
invalidated: oneshot::channel().1,
|
||||
cleanup: SlashCommandCleanup::default(),
|
||||
};
|
||||
};
|
||||
|
||||
let fs = project.fs().clone();
|
||||
let argument = argument.to_string();
|
||||
let output = cx.background_executor().spawn(async move {
|
||||
let content = fs.load(&abs_path).await?;
|
||||
let mut output = String::with_capacity(argument.len() + content.len() + 9);
|
||||
output.push_str("```");
|
||||
output.push_str(&argument);
|
||||
output.push('\n');
|
||||
output.push_str(&content);
|
||||
if !output.ends_with('\n') {
|
||||
output.push('\n');
|
||||
}
|
||||
output.push_str("```");
|
||||
Ok(output)
|
||||
});
|
||||
SlashCommandInvocation {
|
||||
output,
|
||||
invalidated: oneshot::channel().1,
|
||||
cleanup: SlashCommandCleanup::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
|
||||
use crate::prompts::prompt_library::PromptLibrary;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use futures::channel::oneshot;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{AppContext, Task};
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
|
||||
pub(crate) struct PromptSlashCommand {
|
||||
library: Arc<PromptLibrary>,
|
||||
}
|
||||
|
||||
impl PromptSlashCommand {
|
||||
pub fn new(library: Arc<PromptLibrary>) -> Self {
|
||||
Self { library }
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommand for PromptSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"prompt".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert a prompt from the library".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
let library = self.library.clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
let candidates = library
|
||||
.prompts()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(ix, prompt)| {
|
||||
prompt
|
||||
.1
|
||||
.title()
|
||||
.map(|title| StringMatchCandidate::new(ix, title.into()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&cancellation_flag,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
Ok(matches
|
||||
.into_iter()
|
||||
.map(|mat| candidates[mat.candidate_id].string.clone())
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
fn run(&self, title: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation {
|
||||
let Some(title) = title else {
|
||||
return SlashCommandInvocation {
|
||||
output: Task::ready(Err(anyhow!("missing prompt name"))),
|
||||
invalidated: oneshot::channel().1,
|
||||
cleanup: SlashCommandCleanup::default(),
|
||||
};
|
||||
};
|
||||
|
||||
let library = self.library.clone();
|
||||
let title = title.to_string();
|
||||
let output = cx.background_executor().spawn(async move {
|
||||
let prompt = library
|
||||
.prompts()
|
||||
.into_iter()
|
||||
.filter_map(|prompt| prompt.1.title().map(|title| (title, prompt)))
|
||||
.find(|(t, _)| t == &title)
|
||||
.with_context(|| format!("no prompt found with title {:?}", title))?
|
||||
.1;
|
||||
Ok(prompt.1.content().to_owned())
|
||||
});
|
||||
SlashCommandInvocation {
|
||||
output,
|
||||
invalidated: oneshot::channel().1,
|
||||
cleanup: SlashCommandCleanup::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1108,7 +1108,7 @@ impl Render for AssistantChat {
|
||||
.on_click(cx.listener(move |this, _event, cx| {
|
||||
this.new_conversation(cx);
|
||||
}))
|
||||
.tooltip(move |cx| Tooltip::text("New Context", cx)),
|
||||
.tooltip(move |cx| Tooltip::text("New Conversation", cx)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("assistant-menu", IconName::Menu)
|
||||
|
||||
@@ -137,7 +137,6 @@ impl Database {
|
||||
&self,
|
||||
id: DevServerId,
|
||||
name: &str,
|
||||
ssh_connection_string: Option<&str>,
|
||||
user_id: UserId,
|
||||
) -> crate::Result<proto::DevServerProjectsUpdate> {
|
||||
self.transaction(|tx| async move {
|
||||
@@ -150,9 +149,6 @@ impl Database {
|
||||
|
||||
dev_server::Entity::update(dev_server::ActiveModel {
|
||||
name: ActiveValue::Set(name.trim().to_string()),
|
||||
ssh_connection_string: ActiveValue::Set(
|
||||
ssh_connection_string.map(ToOwned::to_owned),
|
||||
),
|
||||
..dev_server.clone().into_active_model()
|
||||
})
|
||||
.exec(&*tx)
|
||||
|
||||
@@ -2439,12 +2439,7 @@ async fn rename_dev_server(
|
||||
let status = session
|
||||
.db()
|
||||
.await
|
||||
.rename_dev_server(
|
||||
dev_server_id,
|
||||
&request.name,
|
||||
request.ssh_connection_string.as_deref(),
|
||||
session.user_id(),
|
||||
)
|
||||
.rename_dev_server(dev_server_id, &request.name, session.user_id())
|
||||
.await?;
|
||||
|
||||
send_dev_server_projects_update(session.user_id(), status, &session).await;
|
||||
|
||||
@@ -352,7 +352,6 @@ async fn test_dev_server_rename(
|
||||
store.rename_dev_server(
|
||||
store.dev_servers().first().unwrap().id,
|
||||
"name-edited".to_string(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -75,17 +75,6 @@ impl CompletionProvider for MessageEditorCompletionProvider {
|
||||
) -> Task<Result<Option<language::Transaction>>> {
|
||||
Task::ready(Ok(None))
|
||||
}
|
||||
|
||||
fn is_completion_trigger(
|
||||
&self,
|
||||
_buffer: &Model<Buffer>,
|
||||
_position: language::Anchor,
|
||||
text: &str,
|
||||
_trigger_in_words: bool,
|
||||
_cx: &mut ViewContext<Editor>,
|
||||
) -> bool {
|
||||
text == "@"
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageEditor {
|
||||
|
||||
@@ -492,8 +492,8 @@ mod tests {
|
||||
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
|
||||
|
||||
// AcceptInlineCompletion when there is an active suggestion inserts it.
|
||||
editor.accept_inline_completion(&Default::default(), cx);
|
||||
// Tabbing when there is an active suggestion inserts it.
|
||||
editor.tab(&Default::default(), cx);
|
||||
assert!(!editor.has_active_inline_completion(cx));
|
||||
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n");
|
||||
@@ -550,8 +550,8 @@ mod tests {
|
||||
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
|
||||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
|
||||
// Using AcceptInlineCompletion again accepts the suggestion.
|
||||
editor.accept_inline_completion(&Default::default(), cx);
|
||||
// Tabbing again accepts the suggestion.
|
||||
editor.tab(&Default::default(), cx);
|
||||
assert!(!editor.has_active_inline_completion(cx));
|
||||
assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
|
||||
@@ -185,7 +185,6 @@ impl Store {
|
||||
&mut self,
|
||||
dev_server_id: DevServerId,
|
||||
name: String,
|
||||
ssh_connection_string: Option<String>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
@@ -194,7 +193,6 @@ impl Store {
|
||||
.request(proto::RenameDevServer {
|
||||
dev_server_id: dev_server_id.0,
|
||||
name,
|
||||
ssh_connection_string,
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
|
||||
@@ -19,7 +19,6 @@ test-support = [
|
||||
"gpui/test-support",
|
||||
"multi_buffer/test-support",
|
||||
"project/test-support",
|
||||
"theme/test-support",
|
||||
"util/test-support",
|
||||
"workspace/test-support",
|
||||
"tree-sitter-rust",
|
||||
@@ -87,7 +86,6 @@ release_channel.workspace = true
|
||||
rand.workspace = true
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
text = { workspace = true, features = ["test-support"] }
|
||||
theme = { workspace = true, features = ["test-support"] }
|
||||
tree-sitter-html.workspace = true
|
||||
tree-sitter-rust.workspace = true
|
||||
tree-sitter-typescript.workspace = true
|
||||
|
||||
@@ -143,7 +143,6 @@ gpui::actions!(
|
||||
editor,
|
||||
[
|
||||
AcceptPartialCopilotSuggestion,
|
||||
AcceptInlineCompletion,
|
||||
AcceptPartialInlineCompletion,
|
||||
AddSelectionAbove,
|
||||
AddSelectionBelow,
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
//! [EditorElement]: crate::element::EditorElement
|
||||
|
||||
mod block_map;
|
||||
mod flap_map;
|
||||
mod fold_map;
|
||||
mod inlay_map;
|
||||
mod tab_map;
|
||||
@@ -29,9 +28,7 @@ use crate::{EditorStyle, RowExt};
|
||||
pub use block_map::{BlockMap, BlockPoint};
|
||||
use collections::{HashMap, HashSet};
|
||||
use fold_map::FoldMap;
|
||||
use gpui::{
|
||||
AnyElement, Font, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, UnderlineStyle,
|
||||
};
|
||||
use gpui::{Font, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, UnderlineStyle};
|
||||
use inlay_map::InlayMap;
|
||||
use language::{
|
||||
language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription,
|
||||
@@ -45,7 +42,6 @@ use serde::Deserialize;
|
||||
use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
|
||||
use sum_tree::{Bias, TreeMap};
|
||||
use tab_map::TabMap;
|
||||
use ui::WindowContext;
|
||||
|
||||
use wrap_map::WrapMap;
|
||||
|
||||
@@ -53,15 +49,10 @@ pub use block_map::{
|
||||
BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockDisposition, BlockId,
|
||||
BlockProperties, BlockStyle, RenderBlock, TransformBlock,
|
||||
};
|
||||
pub use flap_map::*;
|
||||
|
||||
use self::block_map::{BlockRow, BlockSnapshot};
|
||||
use self::fold_map::FoldSnapshot;
|
||||
use self::block_map::BlockRow;
|
||||
pub use self::fold_map::{Fold, FoldId, FoldPoint};
|
||||
use self::inlay_map::InlaySnapshot;
|
||||
pub use self::inlay_map::{InlayOffset, InlayPoint};
|
||||
use self::tab_map::TabSnapshot;
|
||||
use self::wrap_map::WrapSnapshot;
|
||||
pub(crate) use inlay_map::Inlay;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
@@ -70,8 +61,6 @@ pub enum FoldStatus {
|
||||
Foldable,
|
||||
}
|
||||
|
||||
pub type RenderFoldToggle = Arc<dyn Fn(FoldStatus, &mut WindowContext) -> AnyElement>;
|
||||
|
||||
const UNNECESSARY_CODE_FADE: f32 = 0.3;
|
||||
|
||||
pub trait ToDisplayPoint {
|
||||
@@ -103,8 +92,6 @@ pub struct DisplayMap {
|
||||
text_highlights: TextHighlights,
|
||||
/// Regions of inlays that should be highlighted.
|
||||
inlay_highlights: InlayHighlights,
|
||||
/// A container for explicitly foldable ranges, which supersede indentation based fold range suggestions.
|
||||
flap_map: FlapMap,
|
||||
pub clip_at_line_ends: bool,
|
||||
}
|
||||
|
||||
@@ -126,9 +113,7 @@ impl DisplayMap {
|
||||
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
|
||||
let (wrap_map, snapshot) = WrapMap::new(snapshot, font, font_size, wrap_width, cx);
|
||||
let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height);
|
||||
let flap_map = FlapMap::default();
|
||||
cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
|
||||
|
||||
DisplayMap {
|
||||
buffer,
|
||||
buffer_subscription,
|
||||
@@ -137,7 +122,6 @@ impl DisplayMap {
|
||||
tab_map,
|
||||
wrap_map,
|
||||
block_map,
|
||||
flap_map,
|
||||
text_highlights: Default::default(),
|
||||
inlay_highlights: Default::default(),
|
||||
clip_at_line_ends: false,
|
||||
@@ -163,7 +147,6 @@ impl DisplayMap {
|
||||
tab_snapshot,
|
||||
wrap_snapshot,
|
||||
block_snapshot,
|
||||
flap_snapshot: self.flap_map.snapshot(),
|
||||
text_highlights: self.text_highlights.clone(),
|
||||
inlay_highlights: self.inlay_highlights.clone(),
|
||||
clip_at_line_ends: self.clip_at_line_ends,
|
||||
@@ -174,14 +157,14 @@ impl DisplayMap {
|
||||
self.fold(
|
||||
other
|
||||
.folds_in_range(0..other.buffer_snapshot.len())
|
||||
.map(|fold| (fold.range.to_offset(&other.buffer_snapshot), fold.text)),
|
||||
.map(|fold| fold.range.to_offset(&other.buffer_snapshot)),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn fold<T: ToOffset>(
|
||||
&mut self,
|
||||
ranges: impl IntoIterator<Item = (Range<T>, &'static str)>,
|
||||
ranges: impl IntoIterator<Item = Range<T>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
@@ -226,24 +209,6 @@ impl DisplayMap {
|
||||
self.block_map.read(snapshot, edits);
|
||||
}
|
||||
|
||||
pub fn insert_flaps(
|
||||
&mut self,
|
||||
flaps: impl IntoIterator<Item = Flap>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Vec<FlapId> {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
self.flap_map.insert(flaps, &snapshot)
|
||||
}
|
||||
|
||||
pub fn remove_flaps(
|
||||
&mut self,
|
||||
flap_ids: impl IntoIterator<Item = FlapId>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
self.flap_map.remove(flap_ids, &snapshot)
|
||||
}
|
||||
|
||||
pub fn insert_blocks(
|
||||
&mut self,
|
||||
blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
|
||||
@@ -402,12 +367,11 @@ pub struct HighlightedChunk<'a> {
|
||||
#[derive(Clone)]
|
||||
pub struct DisplaySnapshot {
|
||||
pub buffer_snapshot: MultiBufferSnapshot,
|
||||
pub fold_snapshot: FoldSnapshot,
|
||||
pub flap_snapshot: FlapSnapshot,
|
||||
inlay_snapshot: InlaySnapshot,
|
||||
tab_snapshot: TabSnapshot,
|
||||
wrap_snapshot: WrapSnapshot,
|
||||
block_snapshot: BlockSnapshot,
|
||||
pub fold_snapshot: fold_map::FoldSnapshot,
|
||||
inlay_snapshot: inlay_map::InlaySnapshot,
|
||||
tab_snapshot: tab_map::TabSnapshot,
|
||||
wrap_snapshot: wrap_map::WrapSnapshot,
|
||||
block_snapshot: block_map::BlockSnapshot,
|
||||
text_highlights: TextHighlights,
|
||||
inlay_highlights: InlayHighlights,
|
||||
clip_at_line_ends: bool,
|
||||
@@ -869,7 +833,17 @@ impl DisplaySnapshot {
|
||||
DisplayRow(self.block_snapshot.longest_row())
|
||||
}
|
||||
|
||||
pub fn starts_indent(&self, buffer_row: MultiBufferRow) -> bool {
|
||||
pub fn fold_for_line(&self, buffer_row: MultiBufferRow) -> Option<FoldStatus> {
|
||||
if self.is_line_folded(buffer_row) {
|
||||
Some(FoldStatus::Folded)
|
||||
} else if self.is_foldable(buffer_row) {
|
||||
Some(FoldStatus::Foldable)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_foldable(&self, buffer_row: MultiBufferRow) -> bool {
|
||||
let max_row = self.buffer_snapshot.max_buffer_row();
|
||||
if buffer_row >= max_row {
|
||||
return false;
|
||||
@@ -893,17 +867,9 @@ impl DisplaySnapshot {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn foldable_range(
|
||||
&self,
|
||||
buffer_row: MultiBufferRow,
|
||||
) -> Option<(Range<Point>, &'static str)> {
|
||||
pub fn foldable_range(&self, buffer_row: MultiBufferRow) -> Option<Range<Point>> {
|
||||
let start = MultiBufferPoint::new(buffer_row.0, self.buffer_snapshot.line_len(buffer_row));
|
||||
if let Some(flap) = self
|
||||
.flap_snapshot
|
||||
.query_row(buffer_row, &self.buffer_snapshot)
|
||||
{
|
||||
Some((flap.range.to_point(&self.buffer_snapshot), ""))
|
||||
} else if self.starts_indent(MultiBufferRow(start.row))
|
||||
if self.is_foldable(MultiBufferRow(start.row))
|
||||
&& !self.is_line_folded(MultiBufferRow(start.row))
|
||||
{
|
||||
let (start_indent, _) = self.line_indent_for_buffer_row(buffer_row);
|
||||
@@ -922,7 +888,7 @@ impl DisplaySnapshot {
|
||||
}
|
||||
}
|
||||
let end = end.unwrap_or(max_point);
|
||||
Some((start..end, "⋯"))
|
||||
Some(start..end)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -1199,7 +1165,7 @@ pub mod tests {
|
||||
} else {
|
||||
log::info!("folding ranges: {:?}", ranges);
|
||||
map.update(cx, |map, cx| {
|
||||
map.fold(ranges.into_iter().map(|range| (range, "⋯")), cx);
|
||||
map.fold(ranges, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1547,10 +1513,7 @@ pub mod tests {
|
||||
|
||||
map.update(cx, |map, cx| {
|
||||
map.fold(
|
||||
vec![(
|
||||
MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2),
|
||||
"⋯",
|
||||
)],
|
||||
vec![MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2)],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1632,10 +1595,7 @@ pub mod tests {
|
||||
|
||||
map.update(cx, |map, cx| {
|
||||
map.fold(
|
||||
vec![(
|
||||
MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2),
|
||||
"⋯",
|
||||
)],
|
||||
vec![MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2)],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1794,33 +1754,6 @@ pub mod tests {
|
||||
assert("aˇαˇ", cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_flaps(cx: &mut gpui::AppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let text = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll";
|
||||
let buffer = MultiBuffer::build_simple(text, cx);
|
||||
let font_size = px(14.0);
|
||||
cx.new_model(|cx| {
|
||||
let mut map =
|
||||
DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx);
|
||||
let snapshot = map.buffer.read(cx).snapshot(cx);
|
||||
let range =
|
||||
snapshot.anchor_before(Point::new(2, 0))..snapshot.anchor_after(Point::new(3, 3));
|
||||
|
||||
map.flap_map.insert(
|
||||
[Flap::new(
|
||||
range,
|
||||
|_row, _status, _toggle, _cx| div(),
|
||||
|_row, _status, _cx| div(),
|
||||
)],
|
||||
&map.buffer.read(cx).snapshot(cx),
|
||||
);
|
||||
|
||||
map
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_tabs_with_multibyte_chars(cx: &mut gpui::AppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -1,292 +0,0 @@
|
||||
use collections::HashMap;
|
||||
use gpui::{AnyElement, IntoElement};
|
||||
use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, ToPoint};
|
||||
use std::{cmp::Ordering, ops::Range, sync::Arc};
|
||||
use sum_tree::{Bias, SeekTarget, SumTree};
|
||||
use text::Point;
|
||||
use ui::WindowContext;
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
|
||||
pub struct FlapId(usize);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FlapMap {
|
||||
snapshot: FlapSnapshot,
|
||||
next_id: FlapId,
|
||||
id_to_range: HashMap<FlapId, Range<Anchor>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct FlapSnapshot {
|
||||
flaps: SumTree<FlapItem>,
|
||||
}
|
||||
|
||||
impl FlapSnapshot {
|
||||
/// Returns the first Flap starting on the specified buffer row.
|
||||
pub fn query_row<'a>(
|
||||
&'a self,
|
||||
row: MultiBufferRow,
|
||||
snapshot: &'a MultiBufferSnapshot,
|
||||
) -> Option<&'a Flap> {
|
||||
let start = snapshot.anchor_before(Point::new(row.0, 0));
|
||||
let mut cursor = self.flaps.cursor::<ItemSummary>();
|
||||
cursor.seek(&start, Bias::Left, snapshot);
|
||||
while let Some(item) = cursor.item() {
|
||||
match Ord::cmp(&item.flap.range.start.to_point(snapshot).row, &row.0) {
|
||||
Ordering::Less => cursor.next(snapshot),
|
||||
Ordering::Equal => return Some(&item.flap),
|
||||
Ordering::Greater => break,
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
pub fn flap_items_with_offsets(
|
||||
&self,
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
) -> Vec<(FlapId, Range<Point>)> {
|
||||
let mut cursor = self.flaps.cursor::<ItemSummary>();
|
||||
let mut results = Vec::new();
|
||||
|
||||
cursor.next(snapshot);
|
||||
while let Some(item) = cursor.item() {
|
||||
let start_point = item.flap.range.start.to_point(snapshot);
|
||||
let end_point = item.flap.range.end.to_point(snapshot);
|
||||
results.push((item.id, start_point..end_point));
|
||||
cursor.next(snapshot);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
}
|
||||
|
||||
type RenderToggleFn = Arc<
|
||||
dyn Send
|
||||
+ Sync
|
||||
+ Fn(
|
||||
MultiBufferRow,
|
||||
bool,
|
||||
Arc<dyn Send + Sync + Fn(bool, &mut WindowContext)>,
|
||||
&mut WindowContext,
|
||||
) -> AnyElement,
|
||||
>;
|
||||
type RenderTrailerFn =
|
||||
Arc<dyn Send + Sync + Fn(MultiBufferRow, bool, &mut WindowContext) -> AnyElement>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Flap {
|
||||
pub range: Range<Anchor>,
|
||||
pub render_toggle: RenderToggleFn,
|
||||
pub render_trailer: RenderTrailerFn,
|
||||
}
|
||||
|
||||
impl Flap {
|
||||
pub fn new<RenderToggle, ToggleElement, RenderTrailer, TrailerElement>(
|
||||
range: Range<Anchor>,
|
||||
render_toggle: RenderToggle,
|
||||
render_trailer: RenderTrailer,
|
||||
) -> Self
|
||||
where
|
||||
RenderToggle: 'static
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Fn(
|
||||
MultiBufferRow,
|
||||
bool,
|
||||
Arc<dyn Send + Sync + Fn(bool, &mut WindowContext)>,
|
||||
&mut WindowContext,
|
||||
) -> ToggleElement
|
||||
+ 'static,
|
||||
ToggleElement: IntoElement,
|
||||
RenderTrailer: 'static
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Fn(MultiBufferRow, bool, &mut WindowContext) -> TrailerElement
|
||||
+ 'static,
|
||||
TrailerElement: IntoElement,
|
||||
{
|
||||
Flap {
|
||||
range,
|
||||
render_toggle: Arc::new(move |row, folded, toggle, cx| {
|
||||
render_toggle(row, folded, toggle, cx).into_any_element()
|
||||
}),
|
||||
render_trailer: Arc::new(move |row, folded, cx| {
|
||||
render_trailer(row, folded, cx).into_any_element()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Flap {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Flap").field("range", &self.range).finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct FlapItem {
|
||||
id: FlapId,
|
||||
flap: Flap,
|
||||
}
|
||||
|
||||
impl FlapMap {
|
||||
pub fn snapshot(&self) -> FlapSnapshot {
|
||||
self.snapshot.clone()
|
||||
}
|
||||
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
flaps: impl IntoIterator<Item = Flap>,
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
) -> Vec<FlapId> {
|
||||
let mut new_ids = Vec::new();
|
||||
self.snapshot.flaps = {
|
||||
let mut new_flaps = SumTree::new();
|
||||
let mut cursor = self.snapshot.flaps.cursor::<ItemSummary>();
|
||||
for flap in flaps {
|
||||
new_flaps.append(cursor.slice(&flap.range, Bias::Left, snapshot), snapshot);
|
||||
|
||||
let id = self.next_id;
|
||||
self.next_id.0 += 1;
|
||||
self.id_to_range.insert(id, flap.range.clone());
|
||||
new_flaps.push(FlapItem { flap, id }, snapshot);
|
||||
new_ids.push(id);
|
||||
}
|
||||
new_flaps.append(cursor.suffix(snapshot), snapshot);
|
||||
new_flaps
|
||||
};
|
||||
new_ids
|
||||
}
|
||||
|
||||
pub fn remove(
|
||||
&mut self,
|
||||
ids: impl IntoIterator<Item = FlapId>,
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
) {
|
||||
let mut removals = Vec::new();
|
||||
for id in ids {
|
||||
if let Some(range) = self.id_to_range.remove(&id) {
|
||||
removals.push((id, range.clone()));
|
||||
}
|
||||
}
|
||||
removals.sort_unstable_by(|(a_id, a_range), (b_id, b_range)| {
|
||||
AnchorRangeExt::cmp(a_range, b_range, snapshot).then(b_id.cmp(&a_id))
|
||||
});
|
||||
|
||||
self.snapshot.flaps = {
|
||||
let mut new_flaps = SumTree::new();
|
||||
let mut cursor = self.snapshot.flaps.cursor::<ItemSummary>();
|
||||
|
||||
for (id, range) in removals {
|
||||
new_flaps.append(cursor.slice(&range, Bias::Left, snapshot), snapshot);
|
||||
while let Some(item) = cursor.item() {
|
||||
cursor.next(snapshot);
|
||||
if item.id == id {
|
||||
break;
|
||||
} else {
|
||||
new_flaps.push(item.clone(), snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new_flaps.append(cursor.suffix(snapshot), snapshot);
|
||||
new_flaps
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ItemSummary {
|
||||
range: Range<Anchor>,
|
||||
}
|
||||
|
||||
impl Default for ItemSummary {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
range: Anchor::min()..Anchor::min(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Summary for ItemSummary {
|
||||
type Context = MultiBufferSnapshot;
|
||||
|
||||
fn add_summary(&mut self, other: &Self, _snapshot: &MultiBufferSnapshot) {
|
||||
self.range = other.range.clone();
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Item for FlapItem {
|
||||
type Summary = ItemSummary;
|
||||
|
||||
fn summary(&self) -> Self::Summary {
|
||||
ItemSummary {
|
||||
range: self.flap.range.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements `SeekTarget` for `Range<Anchor>` to enable seeking within a `SumTree` of `FlapItem`s.
|
||||
impl SeekTarget<'_, ItemSummary, ItemSummary> for Range<Anchor> {
|
||||
fn cmp(&self, cursor_location: &ItemSummary, snapshot: &MultiBufferSnapshot) -> Ordering {
|
||||
AnchorRangeExt::cmp(self, &cursor_location.range, snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
impl SeekTarget<'_, ItemSummary, ItemSummary> for Anchor {
|
||||
fn cmp(&self, other: &ItemSummary, snapshot: &MultiBufferSnapshot) -> Ordering {
|
||||
self.cmp(&other.range.start, snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use gpui::{div, AppContext};
|
||||
use multi_buffer::MultiBuffer;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_insert_and_remove_flaps(cx: &mut AppContext) {
|
||||
let text = "line1\nline2\nline3\nline4\nline5";
|
||||
let buffer = MultiBuffer::build_simple(text, cx);
|
||||
let snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
|
||||
let mut flap_map = FlapMap::default();
|
||||
|
||||
// Insert flaps
|
||||
let flaps = [
|
||||
Flap::new(
|
||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 5)),
|
||||
|_row, _folded, _toggle, _cx| div(),
|
||||
|_row, _folded, _cx| div(),
|
||||
),
|
||||
Flap::new(
|
||||
snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_after(Point::new(3, 5)),
|
||||
|_row, _folded, _toggle, _cx| div(),
|
||||
|_row, _folded, _cx| div(),
|
||||
),
|
||||
];
|
||||
let flap_ids = flap_map.insert(flaps, &snapshot);
|
||||
assert_eq!(flap_ids.len(), 2);
|
||||
|
||||
// Verify flaps are inserted
|
||||
let flap_snapshot = flap_map.snapshot();
|
||||
assert!(flap_snapshot
|
||||
.query_row(MultiBufferRow(1), &snapshot)
|
||||
.is_some());
|
||||
assert!(flap_snapshot
|
||||
.query_row(MultiBufferRow(3), &snapshot)
|
||||
.is_some());
|
||||
|
||||
// Remove flaps
|
||||
flap_map.remove(flap_ids, &snapshot);
|
||||
|
||||
// Verify flaps are removed
|
||||
let flap_snapshot = flap_map.snapshot();
|
||||
assert!(flap_snapshot
|
||||
.query_row(MultiBufferRow(1), &snapshot)
|
||||
.is_none());
|
||||
assert!(flap_snapshot
|
||||
.query_row(MultiBufferRow(3), &snapshot)
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
@@ -75,12 +75,12 @@ pub(crate) struct FoldMapWriter<'a>(&'a mut FoldMap);
|
||||
impl<'a> FoldMapWriter<'a> {
|
||||
pub(crate) fn fold<T: ToOffset>(
|
||||
&mut self,
|
||||
ranges: impl IntoIterator<Item = (Range<T>, &'static str)>,
|
||||
ranges: impl IntoIterator<Item = Range<T>>,
|
||||
) -> (FoldSnapshot, Vec<FoldEdit>) {
|
||||
let mut edits = Vec::new();
|
||||
let mut folds = Vec::new();
|
||||
let snapshot = self.0.snapshot.inlay_snapshot.clone();
|
||||
for (range, fold_text) in ranges.into_iter() {
|
||||
for range in ranges.into_iter() {
|
||||
let buffer = &snapshot.buffer;
|
||||
let range = range.start.to_offset(&buffer)..range.end.to_offset(&buffer);
|
||||
|
||||
@@ -99,7 +99,6 @@ impl<'a> FoldMapWriter<'a> {
|
||||
folds.push(Fold {
|
||||
id: FoldId(post_inc(&mut self.0.next_fold_id.0)),
|
||||
range: fold_range,
|
||||
text: fold_text,
|
||||
});
|
||||
|
||||
let inlay_range =
|
||||
@@ -325,14 +324,11 @@ impl FoldMap {
|
||||
let mut folds = iter::from_fn({
|
||||
let inlay_snapshot = &inlay_snapshot;
|
||||
move || {
|
||||
let item = folds_cursor.item().map(|fold| {
|
||||
let buffer_start = fold.range.start.to_offset(&inlay_snapshot.buffer);
|
||||
let buffer_end = fold.range.end.to_offset(&inlay_snapshot.buffer);
|
||||
(
|
||||
inlay_snapshot.to_inlay_offset(buffer_start)
|
||||
..inlay_snapshot.to_inlay_offset(buffer_end),
|
||||
fold.text,
|
||||
)
|
||||
let item = folds_cursor.item().map(|f| {
|
||||
let buffer_start = f.range.start.to_offset(&inlay_snapshot.buffer);
|
||||
let buffer_end = f.range.end.to_offset(&inlay_snapshot.buffer);
|
||||
inlay_snapshot.to_inlay_offset(buffer_start)
|
||||
..inlay_snapshot.to_inlay_offset(buffer_end)
|
||||
});
|
||||
folds_cursor.next(&inlay_snapshot.buffer);
|
||||
item
|
||||
@@ -340,27 +336,25 @@ impl FoldMap {
|
||||
})
|
||||
.peekable();
|
||||
|
||||
while folds
|
||||
.peek()
|
||||
.map_or(false, |(fold_range, _)| fold_range.start < edit.new.end)
|
||||
{
|
||||
let (mut fold_range, fold_text) = folds.next().unwrap();
|
||||
while folds.peek().map_or(false, |fold| fold.start < edit.new.end) {
|
||||
let mut fold = folds.next().unwrap();
|
||||
let sum = new_transforms.summary();
|
||||
|
||||
assert!(fold_range.start.0 >= sum.input.len);
|
||||
assert!(fold.start.0 >= sum.input.len);
|
||||
|
||||
while folds.peek().map_or(false, |(next_fold_range, _)| {
|
||||
next_fold_range.start <= fold_range.end
|
||||
}) {
|
||||
let (next_fold_range, _) = folds.next().unwrap();
|
||||
if next_fold_range.end > fold_range.end {
|
||||
fold_range.end = next_fold_range.end;
|
||||
while folds
|
||||
.peek()
|
||||
.map_or(false, |next_fold| next_fold.start <= fold.end)
|
||||
{
|
||||
let next_fold = folds.next().unwrap();
|
||||
if next_fold.end > fold.end {
|
||||
fold.end = next_fold.end;
|
||||
}
|
||||
}
|
||||
|
||||
if fold_range.start.0 > sum.input.len {
|
||||
if fold.start.0 > sum.input.len {
|
||||
let text_summary = inlay_snapshot
|
||||
.text_summary_for_range(InlayOffset(sum.input.len)..fold_range.start);
|
||||
.text_summary_for_range(InlayOffset(sum.input.len)..fold.start);
|
||||
new_transforms.push(
|
||||
Transform {
|
||||
summary: TransformSummary {
|
||||
@@ -373,15 +367,16 @@ impl FoldMap {
|
||||
);
|
||||
}
|
||||
|
||||
if fold_range.end > fold_range.start {
|
||||
if fold.end > fold.start {
|
||||
let output_text = "⋯";
|
||||
new_transforms.push(
|
||||
Transform {
|
||||
summary: TransformSummary {
|
||||
output: TextSummary::from(fold_text),
|
||||
output: TextSummary::from(output_text),
|
||||
input: inlay_snapshot
|
||||
.text_summary_for_range(fold_range.start..fold_range.end),
|
||||
.text_summary_for_range(fold.start..fold.end),
|
||||
},
|
||||
output_text: Some(fold_text),
|
||||
output_text: Some(output_text),
|
||||
},
|
||||
&(),
|
||||
);
|
||||
@@ -858,7 +853,6 @@ impl Into<ElementId> for FoldId {
|
||||
pub struct Fold {
|
||||
pub id: FoldId,
|
||||
pub range: FoldRange,
|
||||
pub text: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -954,7 +948,7 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for FoldRange {
|
||||
|
||||
impl<'a> sum_tree::SeekTarget<'a, FoldSummary, FoldRange> for FoldRange {
|
||||
fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering {
|
||||
AnchorRangeExt::cmp(&self.0, &other.0, buffer)
|
||||
self.0.cmp(&other.0, buffer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1165,8 +1159,8 @@ mod tests {
|
||||
|
||||
let (mut writer, _, _) = map.write(inlay_snapshot, vec![]);
|
||||
let (snapshot2, edits) = writer.fold(vec![
|
||||
(Point::new(0, 2)..Point::new(2, 2), "⋯"),
|
||||
(Point::new(2, 4)..Point::new(4, 1), "⋯"),
|
||||
Point::new(0, 2)..Point::new(2, 2),
|
||||
Point::new(2, 4)..Point::new(4, 1),
|
||||
]);
|
||||
assert_eq!(snapshot2.text(), "aa⋯cc⋯eeeee");
|
||||
assert_eq!(
|
||||
@@ -1245,19 +1239,19 @@ mod tests {
|
||||
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
|
||||
|
||||
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
|
||||
writer.fold(vec![(5..8, "⋯")]);
|
||||
writer.fold(vec![5..8]);
|
||||
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
|
||||
assert_eq!(snapshot.text(), "abcde⋯ijkl");
|
||||
|
||||
// Create an fold adjacent to the start of the first fold.
|
||||
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
|
||||
writer.fold(vec![(0..1, "⋯"), (2..5, "⋯")]);
|
||||
writer.fold(vec![0..1, 2..5]);
|
||||
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
|
||||
assert_eq!(snapshot.text(), "⋯b⋯ijkl");
|
||||
|
||||
// Create an fold adjacent to the end of the first fold.
|
||||
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
|
||||
writer.fold(vec![(11..11, "⋯"), (8..10, "⋯")]);
|
||||
writer.fold(vec![11..11, 8..10]);
|
||||
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
|
||||
assert_eq!(snapshot.text(), "⋯b⋯kl");
|
||||
}
|
||||
@@ -1267,7 +1261,7 @@ mod tests {
|
||||
|
||||
// Create two adjacent folds.
|
||||
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
|
||||
writer.fold(vec![(0..2, "⋯"), (2..5, "⋯")]);
|
||||
writer.fold(vec![0..2, 2..5]);
|
||||
let (snapshot, _) = map.read(inlay_snapshot, vec![]);
|
||||
assert_eq!(snapshot.text(), "⋯fghijkl");
|
||||
|
||||
@@ -1291,10 +1285,10 @@ mod tests {
|
||||
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
|
||||
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
|
||||
writer.fold(vec![
|
||||
(Point::new(0, 2)..Point::new(2, 2), "⋯"),
|
||||
(Point::new(0, 4)..Point::new(1, 0), "⋯"),
|
||||
(Point::new(1, 2)..Point::new(3, 2), "⋯"),
|
||||
(Point::new(3, 1)..Point::new(4, 1), "⋯"),
|
||||
Point::new(0, 2)..Point::new(2, 2),
|
||||
Point::new(0, 4)..Point::new(1, 0),
|
||||
Point::new(1, 2)..Point::new(3, 2),
|
||||
Point::new(3, 1)..Point::new(4, 1),
|
||||
]);
|
||||
let (snapshot, _) = map.read(inlay_snapshot, vec![]);
|
||||
assert_eq!(snapshot.text(), "aa⋯eeeee");
|
||||
@@ -1311,8 +1305,8 @@ mod tests {
|
||||
|
||||
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
|
||||
writer.fold(vec![
|
||||
(Point::new(0, 2)..Point::new(2, 2), "⋯"),
|
||||
(Point::new(3, 1)..Point::new(4, 1), "⋯"),
|
||||
Point::new(0, 2)..Point::new(2, 2),
|
||||
Point::new(3, 1)..Point::new(4, 1),
|
||||
]);
|
||||
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
|
||||
assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee");
|
||||
@@ -1336,10 +1330,10 @@ mod tests {
|
||||
|
||||
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
|
||||
writer.fold(vec![
|
||||
(Point::new(0, 2)..Point::new(2, 2), "⋯"),
|
||||
(Point::new(0, 4)..Point::new(1, 0), "⋯"),
|
||||
(Point::new(1, 2)..Point::new(3, 2), "⋯"),
|
||||
(Point::new(3, 1)..Point::new(4, 1), "⋯"),
|
||||
Point::new(0, 2)..Point::new(2, 2),
|
||||
Point::new(0, 4)..Point::new(1, 0),
|
||||
Point::new(1, 2)..Point::new(3, 2),
|
||||
Point::new(3, 1)..Point::new(4, 1),
|
||||
]);
|
||||
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
|
||||
let fold_ranges = snapshot
|
||||
@@ -1414,10 +1408,10 @@ mod tests {
|
||||
snapshot_edits.push((snapshot.clone(), edits));
|
||||
|
||||
let mut expected_text: String = inlay_snapshot.text().to_string();
|
||||
for (fold_range, fold_text) in map.merged_folds().into_iter().rev() {
|
||||
for fold_range in map.merged_fold_ranges().into_iter().rev() {
|
||||
let fold_inlay_start = inlay_snapshot.to_inlay_offset(fold_range.start);
|
||||
let fold_inlay_end = inlay_snapshot.to_inlay_offset(fold_range.end);
|
||||
expected_text.replace_range(fold_inlay_start.0..fold_inlay_end.0, fold_text);
|
||||
expected_text.replace_range(fold_inlay_start.0..fold_inlay_end.0, "⋯");
|
||||
}
|
||||
|
||||
assert_eq!(snapshot.text(), expected_text);
|
||||
@@ -1429,7 +1423,7 @@ mod tests {
|
||||
|
||||
let mut prev_row = 0;
|
||||
let mut expected_buffer_rows = Vec::new();
|
||||
for (fold_range, _fold_text) in map.merged_folds().into_iter() {
|
||||
for fold_range in map.merged_fold_ranges().into_iter() {
|
||||
let fold_start = inlay_snapshot
|
||||
.to_point(inlay_snapshot.to_inlay_offset(fold_range.start))
|
||||
.row();
|
||||
@@ -1541,11 +1535,11 @@ mod tests {
|
||||
}
|
||||
|
||||
let folded_buffer_rows = map
|
||||
.merged_folds()
|
||||
.merged_fold_ranges()
|
||||
.iter()
|
||||
.flat_map(|(fold_range, _)| {
|
||||
let start_row = fold_range.start.to_point(&buffer_snapshot).row;
|
||||
let end = fold_range.end.to_point(&buffer_snapshot);
|
||||
.flat_map(|range| {
|
||||
let start_row = range.start.to_point(&buffer_snapshot).row;
|
||||
let end = range.end.to_point(&buffer_snapshot);
|
||||
if end.column == 0 {
|
||||
start_row..end.row
|
||||
} else {
|
||||
@@ -1640,8 +1634,8 @@ mod tests {
|
||||
|
||||
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
|
||||
writer.fold(vec![
|
||||
(Point::new(0, 2)..Point::new(2, 2), "⋯"),
|
||||
(Point::new(3, 1)..Point::new(4, 1), "⋯"),
|
||||
Point::new(0, 2)..Point::new(2, 2),
|
||||
Point::new(3, 1)..Point::new(4, 1),
|
||||
]);
|
||||
|
||||
let (snapshot, _) = map.read(inlay_snapshot, vec![]);
|
||||
@@ -1659,39 +1653,34 @@ mod tests {
|
||||
}
|
||||
|
||||
impl FoldMap {
|
||||
fn merged_folds(&self) -> Vec<(Range<usize>, &'static str)> {
|
||||
fn merged_fold_ranges(&self) -> Vec<Range<usize>> {
|
||||
let inlay_snapshot = self.snapshot.inlay_snapshot.clone();
|
||||
let buffer = &inlay_snapshot.buffer;
|
||||
let mut folds = self.snapshot.folds.items(buffer);
|
||||
// Ensure sorting doesn't change how folds get merged and displayed.
|
||||
folds.sort_by(|a, b| a.range.cmp(&b.range, buffer));
|
||||
let mut folds = folds
|
||||
let mut fold_ranges = folds
|
||||
.iter()
|
||||
.map(|fold| {
|
||||
(
|
||||
fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer),
|
||||
fold.text,
|
||||
)
|
||||
})
|
||||
.map(|fold| fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer))
|
||||
.peekable();
|
||||
|
||||
let mut merged_folds = Vec::new();
|
||||
while let Some((mut fold_range, fold_text)) = folds.next() {
|
||||
while let Some((next_range, _)) = folds.peek() {
|
||||
let mut merged_ranges = Vec::new();
|
||||
while let Some(mut fold_range) = fold_ranges.next() {
|
||||
while let Some(next_range) = fold_ranges.peek() {
|
||||
if fold_range.end >= next_range.start {
|
||||
if next_range.end > fold_range.end {
|
||||
fold_range.end = next_range.end;
|
||||
}
|
||||
folds.next();
|
||||
fold_ranges.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if fold_range.end > fold_range.start {
|
||||
merged_folds.push((fold_range, fold_text));
|
||||
merged_ranges.push(fold_range);
|
||||
}
|
||||
}
|
||||
merged_folds
|
||||
merged_ranges
|
||||
}
|
||||
|
||||
pub fn randomly_mutate(
|
||||
@@ -1709,11 +1698,10 @@ mod tests {
|
||||
let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
|
||||
to_unfold.push(start..end);
|
||||
}
|
||||
let inclusive = rng.gen();
|
||||
log::info!("unfolding {:?} (inclusive: {})", to_unfold, inclusive);
|
||||
log::info!("unfolding {:?}", to_unfold);
|
||||
let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]);
|
||||
snapshot_edits.push((snapshot, edits));
|
||||
let (snapshot, edits) = writer.unfold(to_unfold, inclusive);
|
||||
let (snapshot, edits) = writer.fold(to_unfold);
|
||||
snapshot_edits.push((snapshot, edits));
|
||||
}
|
||||
_ => {
|
||||
@@ -1723,8 +1711,7 @@ mod tests {
|
||||
for _ in 0..rng.gen_range(1..=2) {
|
||||
let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
|
||||
let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
|
||||
let text = if rng.gen() { "⋯" } else { "" };
|
||||
to_fold.push((start..end, text));
|
||||
to_fold.push(start..end);
|
||||
}
|
||||
log::info!("folding {:?}", to_fold);
|
||||
let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]);
|
||||
|
||||
@@ -406,7 +406,6 @@ struct RunnableTasks {
|
||||
templates: Vec<(TaskSourceKind, TaskTemplate)>,
|
||||
// We need the column at which the task context evaluation should take place.
|
||||
column: u32,
|
||||
// Values of all named captures, including those starting with '_'
|
||||
extra_variables: HashMap<String, String>,
|
||||
}
|
||||
|
||||
@@ -449,9 +448,6 @@ pub struct Editor {
|
||||
mode: EditorMode,
|
||||
show_breadcrumbs: bool,
|
||||
show_gutter: bool,
|
||||
show_line_numbers: Option<bool>,
|
||||
show_git_diff_gutter: Option<bool>,
|
||||
show_code_actions: Option<bool>,
|
||||
show_wrap_guides: Option<bool>,
|
||||
placeholder_text: Option<Arc<str>>,
|
||||
highlight_order: usize,
|
||||
@@ -520,9 +516,6 @@ pub struct Editor {
|
||||
pub struct EditorSnapshot {
|
||||
pub mode: EditorMode,
|
||||
show_gutter: bool,
|
||||
show_line_numbers: Option<bool>,
|
||||
show_git_diff_gutter: Option<bool>,
|
||||
show_code_actions: Option<bool>,
|
||||
render_git_blame_gutter: bool,
|
||||
pub display_snapshot: DisplaySnapshot,
|
||||
pub placeholder_text: Option<Arc<str>>,
|
||||
@@ -530,7 +523,6 @@ pub struct EditorSnapshot {
|
||||
scroll_anchor: ScrollAnchor,
|
||||
ongoing_scroll: OngoingScroll,
|
||||
current_line_highlight: CurrentLineHighlight,
|
||||
gutter_hovered: bool,
|
||||
}
|
||||
|
||||
const GIT_BLAME_GUTTER_WIDTH_CHARS: f32 = 53.;
|
||||
@@ -1652,9 +1644,6 @@ impl Editor {
|
||||
mode,
|
||||
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
|
||||
show_gutter: mode == EditorMode::Full,
|
||||
show_line_numbers: None,
|
||||
show_git_diff_gutter: None,
|
||||
show_code_actions: None,
|
||||
show_wrap_guides: None,
|
||||
placeholder_text: None,
|
||||
highlight_order: 0,
|
||||
@@ -1890,9 +1879,6 @@ impl Editor {
|
||||
EditorSnapshot {
|
||||
mode: self.mode,
|
||||
show_gutter: self.show_gutter,
|
||||
show_line_numbers: self.show_line_numbers,
|
||||
show_git_diff_gutter: self.show_git_diff_gutter,
|
||||
show_code_actions: self.show_code_actions,
|
||||
render_git_blame_gutter: self.render_git_blame_gutter(cx),
|
||||
display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
|
||||
scroll_anchor: self.scroll_manager.anchor(),
|
||||
@@ -1900,7 +1886,6 @@ impl Editor {
|
||||
placeholder_text: self.placeholder_text.clone(),
|
||||
is_focused: self.focus_handle.is_focused(cx),
|
||||
current_line_highlight: self.current_line_highlight,
|
||||
gutter_hovered: self.gutter_hovered,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1945,8 +1930,8 @@ impl Editor {
|
||||
self.custom_context_menu = Some(Box::new(f))
|
||||
}
|
||||
|
||||
pub fn set_completion_provider(&mut self, provider: Box<dyn CompletionProvider>) {
|
||||
self.completion_provider = Some(provider);
|
||||
pub fn set_completion_provider(&mut self, hub: Box<dyn CompletionProvider>) {
|
||||
self.completion_provider = Some(hub);
|
||||
}
|
||||
|
||||
pub fn set_inline_completion_provider<T>(
|
||||
@@ -3292,41 +3277,22 @@ impl Editor {
|
||||
trigger_in_words: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if self.is_completion_trigger(text, trigger_in_words, cx) {
|
||||
if !EditorSettings::get_global(cx).show_completions_on_input {
|
||||
return;
|
||||
}
|
||||
|
||||
let selection = self.selections.newest_anchor();
|
||||
if self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.is_completion_trigger(selection.head(), text, trigger_in_words, cx)
|
||||
{
|
||||
self.show_completions(&ShowCompletions, cx);
|
||||
} else {
|
||||
self.hide_context_menu(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_completion_trigger(
|
||||
&self,
|
||||
text: &str,
|
||||
trigger_in_words: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool {
|
||||
let position = self.selections.newest_anchor().head();
|
||||
let multibuffer = self.buffer.read(cx);
|
||||
let Some(buffer) = position
|
||||
.buffer_id
|
||||
.and_then(|buffer_id| multibuffer.buffer(buffer_id).clone())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if let Some(completion_provider) = &self.completion_provider {
|
||||
completion_provider.is_completion_trigger(
|
||||
&buffer,
|
||||
position.text_anchor,
|
||||
text,
|
||||
trigger_in_words,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// If any empty selections is touching the start of its innermost containing autoclose
|
||||
/// region, expand it to select the brackets.
|
||||
fn select_autoclose_pair(&mut self, cx: &mut ViewContext<Self>) {
|
||||
@@ -4005,7 +3971,7 @@ impl Editor {
|
||||
|
||||
this.completion_tasks.clear();
|
||||
this.discard_inline_completion(false, cx);
|
||||
let tasks = tasks.as_ref().zip(this.workspace.clone()).and_then(
|
||||
let task_context = tasks.as_ref().zip(this.workspace.clone()).and_then(
|
||||
|(tasks, (workspace, _))| {
|
||||
let position = Point::new(buffer_row, tasks.1.column);
|
||||
let range_start = buffer.read(cx).anchor_at(position, Bias::Right);
|
||||
@@ -4013,45 +3979,43 @@ impl Editor {
|
||||
buffer: buffer.clone(),
|
||||
range: range_start..range_start,
|
||||
};
|
||||
// Fill in the environmental variables from the tree-sitter captures
|
||||
let mut captured_task_variables = TaskVariables::default();
|
||||
for (capture_name, value) in tasks.1.extra_variables.clone() {
|
||||
captured_task_variables.insert(
|
||||
task::VariableName::Custom(capture_name.into()),
|
||||
value.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
tasks::task_context_for_location(
|
||||
captured_task_variables,
|
||||
workspace,
|
||||
location,
|
||||
cx,
|
||||
)
|
||||
tasks::task_context_for_location(workspace, location, cx)
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|task_context| {
|
||||
Arc::new(ResolvedTasks {
|
||||
templates: tasks
|
||||
.1
|
||||
.templates
|
||||
.iter()
|
||||
.filter_map(|(kind, template)| {
|
||||
template
|
||||
.resolve_task(&kind.to_id_base(), &task_context)
|
||||
.map(|task| (kind.clone(), task))
|
||||
})
|
||||
.collect(),
|
||||
position: snapshot.buffer_snapshot.anchor_before(
|
||||
Point::new(multibuffer_point.row, tasks.1.column),
|
||||
),
|
||||
})
|
||||
})
|
||||
},
|
||||
);
|
||||
let tasks = tasks.zip(task_context).map(|(tasks, mut task_context)| {
|
||||
// Fill in the environmental variables from the tree-sitter captures
|
||||
let mut additional_task_variables = TaskVariables::default();
|
||||
for (capture_name, value) in tasks.1.extra_variables.clone() {
|
||||
additional_task_variables.insert(
|
||||
task::VariableName::Custom(capture_name.into()),
|
||||
value.clone(),
|
||||
);
|
||||
}
|
||||
task_context
|
||||
.task_variables
|
||||
.extend(additional_task_variables);
|
||||
|
||||
Arc::new(ResolvedTasks {
|
||||
templates: tasks
|
||||
.1
|
||||
.templates
|
||||
.iter()
|
||||
.filter_map(|(kind, template)| {
|
||||
template
|
||||
.resolve_task(&kind.to_id_base(), &task_context)
|
||||
.map(|task| (kind.clone(), task))
|
||||
})
|
||||
.collect(),
|
||||
position: snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(Point::new(multibuffer_point.row, tasks.1.column)),
|
||||
})
|
||||
});
|
||||
let spawn_straight_away = tasks
|
||||
.as_ref()
|
||||
.map_or(false, |tasks| tasks.templates.len() == 1)
|
||||
@@ -4484,25 +4448,23 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn accept_inline_completion(
|
||||
&mut self,
|
||||
_: &AcceptInlineCompletion,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let Some(completion) = self.take_active_inline_completion(cx) else {
|
||||
return;
|
||||
};
|
||||
if let Some(provider) = self.inline_completion_provider() {
|
||||
provider.accept(cx);
|
||||
}
|
||||
fn accept_inline_completion(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
if let Some(completion) = self.take_active_inline_completion(cx) {
|
||||
if let Some(provider) = self.inline_completion_provider() {
|
||||
provider.accept(cx);
|
||||
}
|
||||
|
||||
cx.emit(EditorEvent::InputHandled {
|
||||
utf16_range_to_replace: None,
|
||||
text: completion.text.to_string().into(),
|
||||
});
|
||||
self.insert_with_autoindent_mode(&completion.text.to_string(), None, cx);
|
||||
self.refresh_inline_completion(true, cx);
|
||||
cx.notify();
|
||||
cx.emit(EditorEvent::InputHandled {
|
||||
utf16_range_to_replace: None,
|
||||
text: completion.text.to_string().into(),
|
||||
});
|
||||
self.insert_with_autoindent_mode(&completion.text.to_string(), None, cx);
|
||||
self.refresh_inline_completion(true, cx);
|
||||
cx.notify();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn accept_partial_inline_completion(
|
||||
@@ -4677,6 +4639,44 @@ impl Editor {
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn render_fold_indicators(
|
||||
&mut self,
|
||||
fold_data: Vec<Option<(FoldStatus, MultiBufferRow, bool)>>,
|
||||
_style: &EditorStyle,
|
||||
gutter_hovered: bool,
|
||||
_line_height: Pixels,
|
||||
_gutter_margin: Pixels,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Vec<Option<AnyElement>> {
|
||||
fold_data
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, fold_data)| {
|
||||
fold_data
|
||||
.map(|(fold_status, buffer_row, active)| {
|
||||
(active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
|
||||
IconButton::new(ix, ui::IconName::ChevronDown)
|
||||
.on_click(cx.listener(move |this, _e, cx| match fold_status {
|
||||
FoldStatus::Folded => {
|
||||
this.unfold_at(&UnfoldAt { buffer_row }, cx);
|
||||
}
|
||||
FoldStatus::Foldable => {
|
||||
this.fold_at(&FoldAt { buffer_row }, cx);
|
||||
}
|
||||
}))
|
||||
.icon_color(ui::Color::Muted)
|
||||
.icon_size(ui::IconSize::Small)
|
||||
.selected(fold_status == FoldStatus::Folded)
|
||||
.selected_icon(ui::IconName::ChevronRight)
|
||||
.size(ui::ButtonSize::None)
|
||||
.into_any_element()
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn context_menu_visible(&self) -> bool {
|
||||
self.context_menu
|
||||
.read()
|
||||
@@ -5002,6 +5002,16 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
// Accept copilot completion if there is only one selection and the cursor is not
|
||||
// in the leading whitespace.
|
||||
if self.selections.count() == 1
|
||||
&& cursor.column >= current_indent.len
|
||||
&& self.has_active_inline_completion(cx)
|
||||
{
|
||||
self.accept_inline_completion(cx);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, insert a hard or soft tab.
|
||||
let settings = buffer.settings_at(cursor, cx);
|
||||
let tab_size = if settings.hard_tabs {
|
||||
@@ -5820,7 +5830,7 @@ impl Editor {
|
||||
let mut end = fold.range.end.to_point(&buffer);
|
||||
start.row -= row_delta;
|
||||
end.row -= row_delta;
|
||||
refold_ranges.push((start..end, fold.text));
|
||||
refold_ranges.push(start..end);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5914,7 +5924,7 @@ impl Editor {
|
||||
let mut end = fold.range.end.to_point(&buffer);
|
||||
start.row += row_delta;
|
||||
end.row += row_delta;
|
||||
refold_ranges.push((start..end, fold.text));
|
||||
refold_ranges.push(start..end);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9272,11 +9282,11 @@ impl Editor {
|
||||
let buffer_start_row = range.start.row;
|
||||
|
||||
for row in (0..=range.end.row).rev() {
|
||||
if let Some((foldable_range, fold_text)) =
|
||||
display_map.foldable_range(MultiBufferRow(row))
|
||||
{
|
||||
if foldable_range.end.row >= buffer_start_row {
|
||||
fold_ranges.push((foldable_range, fold_text));
|
||||
let fold_range = display_map.foldable_range(MultiBufferRow(row));
|
||||
|
||||
if let Some(fold_range) = fold_range {
|
||||
if fold_range.end.row >= buffer_start_row {
|
||||
fold_ranges.push(fold_range);
|
||||
if row <= range.start.row {
|
||||
break;
|
||||
}
|
||||
@@ -9292,14 +9302,14 @@ impl Editor {
|
||||
let buffer_row = fold_at.buffer_row;
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
if let Some((fold_range, fold_text)) = display_map.foldable_range(buffer_row) {
|
||||
if let Some(fold_range) = display_map.foldable_range(buffer_row) {
|
||||
let autoscroll = self
|
||||
.selections
|
||||
.all::<Point>(cx)
|
||||
.iter()
|
||||
.any(|selection| fold_range.overlaps(&selection.range()));
|
||||
|
||||
self.fold_ranges([(fold_range, fold_text)], autoscroll, cx);
|
||||
self.fold_ranges(std::iter::once(fold_range), autoscroll, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9353,9 +9363,9 @@ impl Editor {
|
||||
.buffer_snapshot
|
||||
.line_len(MultiBufferRow(s.end.row)),
|
||||
);
|
||||
(start..end, "⋯")
|
||||
start..end
|
||||
} else {
|
||||
(s.start..s.end, "⋯")
|
||||
s.start..s.end
|
||||
}
|
||||
});
|
||||
self.fold_ranges(ranges, true, cx);
|
||||
@@ -9363,20 +9373,18 @@ impl Editor {
|
||||
|
||||
pub fn fold_ranges<T: ToOffset + Clone>(
|
||||
&mut self,
|
||||
ranges: impl IntoIterator<Item = (Range<T>, &'static str)>,
|
||||
ranges: impl IntoIterator<Item = Range<T>>,
|
||||
auto_scroll: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let mut fold_ranges = Vec::new();
|
||||
let mut buffers_affected = HashMap::default();
|
||||
let multi_buffer = self.buffer().read(cx);
|
||||
for (fold_range, fold_text) in ranges {
|
||||
if let Some((_, buffer, _)) =
|
||||
multi_buffer.excerpt_containing(fold_range.start.clone(), cx)
|
||||
{
|
||||
for range in ranges {
|
||||
if let Some((_, buffer, _)) = multi_buffer.excerpt_containing(range.start.clone(), cx) {
|
||||
buffers_affected.insert(buffer.read(cx).remote_id(), buffer);
|
||||
};
|
||||
fold_ranges.push((fold_range, fold_text));
|
||||
fold_ranges.push(range);
|
||||
}
|
||||
|
||||
let mut ranges = fold_ranges.into_iter().peekable();
|
||||
@@ -9492,24 +9500,6 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_flaps(
|
||||
&mut self,
|
||||
flaps: impl IntoIterator<Item = Flap>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Vec<FlapId> {
|
||||
self.display_map
|
||||
.update(cx, |map, cx| map.insert_flaps(flaps, cx))
|
||||
}
|
||||
|
||||
pub fn remove_flaps(
|
||||
&mut self,
|
||||
ids: impl IntoIterator<Item = FlapId>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.display_map
|
||||
.update(cx, |map, cx| map.remove_flaps(ids, cx));
|
||||
}
|
||||
|
||||
pub fn longest_row(&self, cx: &mut AppContext) -> DisplayRow {
|
||||
self.display_map
|
||||
.update(cx, |map, cx| map.snapshot(cx))
|
||||
@@ -9644,27 +9634,8 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_show_line_numbers(&mut self, show_line_numbers: bool, cx: &mut ViewContext<Self>) {
|
||||
self.show_line_numbers = Some(show_line_numbers);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_show_git_diff_gutter(
|
||||
&mut self,
|
||||
show_git_diff_gutter: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.show_git_diff_gutter = Some(show_git_diff_gutter);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_show_code_actions(&mut self, show_code_actions: bool, cx: &mut ViewContext<Self>) {
|
||||
self.show_code_actions = Some(show_code_actions);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_show_wrap_guides(&mut self, show_wrap_guides: bool, cx: &mut ViewContext<Self>) {
|
||||
self.show_wrap_guides = Some(show_wrap_guides);
|
||||
pub fn set_show_wrap_guides(&mut self, show_gutter: bool, cx: &mut ViewContext<Self>) {
|
||||
self.show_wrap_guides = Some(show_gutter);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -10938,15 +10909,6 @@ pub trait CompletionProvider {
|
||||
push_to_history: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<Option<language::Transaction>>>;
|
||||
|
||||
fn is_completion_trigger(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
position: language::Anchor,
|
||||
text: &str,
|
||||
trigger_in_words: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> bool;
|
||||
}
|
||||
|
||||
impl CompletionProvider for Model<Project> {
|
||||
@@ -10984,40 +10946,6 @@ impl CompletionProvider for Model<Project> {
|
||||
project.apply_additional_edits_for_completion(buffer, completion, push_to_history, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn is_completion_trigger(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
position: language::Anchor,
|
||||
text: &str,
|
||||
trigger_in_words: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> bool {
|
||||
if !EditorSettings::get_global(cx).show_completions_on_input {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut chars = text.chars();
|
||||
let char = if let Some(char) = chars.next() {
|
||||
char
|
||||
} else {
|
||||
return false;
|
||||
};
|
||||
if chars.next().is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let buffer = buffer.read(cx);
|
||||
let scope = buffer.snapshot().language_scope_at(position);
|
||||
if trigger_in_words && char_kind(&scope, char) == CharKind::Word {
|
||||
return true;
|
||||
}
|
||||
|
||||
buffer
|
||||
.completion_triggers()
|
||||
.iter()
|
||||
.any(|string| string == text)
|
||||
}
|
||||
}
|
||||
|
||||
fn inlay_hint_settings(
|
||||
@@ -11123,17 +11051,13 @@ impl EditorSnapshot {
|
||||
}
|
||||
let descent = cx.text_system().descent(font_id, font_size);
|
||||
|
||||
let show_git_gutter = self.show_git_diff_gutter.unwrap_or_else(|| {
|
||||
matches!(
|
||||
ProjectSettings::get_global(cx).git.git_gutter,
|
||||
Some(GitGutterSetting::TrackedFiles)
|
||||
)
|
||||
});
|
||||
let show_git_gutter = matches!(
|
||||
ProjectSettings::get_global(cx).git.git_gutter,
|
||||
Some(GitGutterSetting::TrackedFiles)
|
||||
);
|
||||
let gutter_settings = EditorSettings::get_global(cx).gutter;
|
||||
let show_line_numbers = self
|
||||
.show_line_numbers
|
||||
.unwrap_or_else(|| gutter_settings.line_numbers);
|
||||
let line_gutter_width = if show_line_numbers {
|
||||
let gutter_lines_enabled = gutter_settings.line_numbers;
|
||||
let line_gutter_width = if gutter_lines_enabled {
|
||||
// Avoid flicker-like gutter resizes when the line number gains another digit and only resize the gutter on files with N*10^5 lines.
|
||||
let min_width_for_number_on_gutter = em_width * 4.0;
|
||||
max_line_number_width.max(min_width_for_number_on_gutter)
|
||||
@@ -11141,30 +11065,26 @@ impl EditorSnapshot {
|
||||
0.0.into()
|
||||
};
|
||||
|
||||
let show_code_actions = self
|
||||
.show_code_actions
|
||||
.unwrap_or_else(|| gutter_settings.code_actions);
|
||||
|
||||
let git_blame_entries_width = self
|
||||
.render_git_blame_gutter
|
||||
.then_some(em_width * GIT_BLAME_GUTTER_WIDTH_CHARS);
|
||||
|
||||
let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO);
|
||||
left_padding += if show_code_actions {
|
||||
left_padding += if gutter_settings.code_actions {
|
||||
em_width * 3.0
|
||||
} else if show_git_gutter && show_line_numbers {
|
||||
} else if show_git_gutter && gutter_lines_enabled {
|
||||
em_width * 2.0
|
||||
} else if show_git_gutter || show_line_numbers {
|
||||
} else if show_git_gutter || gutter_lines_enabled {
|
||||
em_width
|
||||
} else {
|
||||
px(0.)
|
||||
};
|
||||
|
||||
let right_padding = if gutter_settings.folds && show_line_numbers {
|
||||
let right_padding = if gutter_settings.folds && gutter_lines_enabled {
|
||||
em_width * 4.0
|
||||
} else if gutter_settings.folds {
|
||||
em_width * 3.0
|
||||
} else if show_line_numbers {
|
||||
} else if gutter_lines_enabled {
|
||||
em_width
|
||||
} else {
|
||||
px(0.)
|
||||
@@ -11178,76 +11098,6 @@ impl EditorSnapshot {
|
||||
git_blame_entries_width,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_fold_toggle(
|
||||
&self,
|
||||
buffer_row: MultiBufferRow,
|
||||
row_contains_cursor: bool,
|
||||
editor: View<Editor>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<AnyElement> {
|
||||
let folded = self.is_line_folded(buffer_row);
|
||||
|
||||
if let Some(flap) = self
|
||||
.flap_snapshot
|
||||
.query_row(buffer_row, &self.buffer_snapshot)
|
||||
{
|
||||
let toggle_callback = Arc::new(move |folded, cx: &mut WindowContext| {
|
||||
if folded {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.fold_at(&crate::FoldAt { buffer_row }, cx)
|
||||
});
|
||||
} else {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.unfold_at(&crate::UnfoldAt { buffer_row }, cx)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Some((flap.render_toggle)(
|
||||
buffer_row,
|
||||
folded,
|
||||
toggle_callback,
|
||||
cx,
|
||||
))
|
||||
} else if folded
|
||||
|| (self.starts_indent(buffer_row) && (row_contains_cursor || self.gutter_hovered))
|
||||
{
|
||||
Some(
|
||||
IconButton::new(
|
||||
("indent-fold-indicator", buffer_row.0),
|
||||
ui::IconName::ChevronDown,
|
||||
)
|
||||
.on_click(cx.listener_for(&editor, move |this, _e, cx| {
|
||||
if folded {
|
||||
this.unfold_at(&UnfoldAt { buffer_row }, cx);
|
||||
} else {
|
||||
this.fold_at(&FoldAt { buffer_row }, cx);
|
||||
}
|
||||
}))
|
||||
.icon_color(ui::Color::Muted)
|
||||
.icon_size(ui::IconSize::Small)
|
||||
.selected(folded)
|
||||
.selected_icon(ui::IconName::ChevronRight)
|
||||
.size(ui::ButtonSize::None)
|
||||
.into_any_element(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_flap_trailer(
|
||||
&self,
|
||||
buffer_row: MultiBufferRow,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<AnyElement> {
|
||||
let folded = self.is_line_folded(buffer_row);
|
||||
let flap = self
|
||||
.flap_snapshot
|
||||
.query_row(buffer_row, &self.buffer_snapshot)?;
|
||||
Some((flap.render_trailer)(buffer_row, folded, cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for EditorSnapshot {
|
||||
|
||||
@@ -494,8 +494,8 @@ fn test_clone(cx: &mut TestAppContext) {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone()));
|
||||
editor.fold_ranges(
|
||||
[
|
||||
(Point::new(1, 0)..Point::new(2, 0), "⋯"),
|
||||
(Point::new(3, 0)..Point::new(4, 0), "⋯"),
|
||||
Point::new(1, 0)..Point::new(2, 0),
|
||||
Point::new(3, 0)..Point::new(4, 0),
|
||||
],
|
||||
true,
|
||||
cx,
|
||||
@@ -903,9 +903,9 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
|
||||
_ = view.update(cx, |view, cx| {
|
||||
view.fold_ranges(
|
||||
vec![
|
||||
(Point::new(0, 6)..Point::new(0, 12), "⋯"),
|
||||
(Point::new(1, 2)..Point::new(1, 4), "⋯"),
|
||||
(Point::new(2, 4)..Point::new(2, 8), "⋯"),
|
||||
Point::new(0, 6)..Point::new(0, 12),
|
||||
Point::new(1, 2)..Point::new(1, 4),
|
||||
Point::new(2, 4)..Point::new(2, 8),
|
||||
],
|
||||
true,
|
||||
cx,
|
||||
@@ -3407,9 +3407,9 @@ fn test_move_line_up_down(cx: &mut TestAppContext) {
|
||||
_ = view.update(cx, |view, cx| {
|
||||
view.fold_ranges(
|
||||
vec![
|
||||
(Point::new(0, 2)..Point::new(1, 2), "⋯"),
|
||||
(Point::new(2, 3)..Point::new(4, 1), "⋯"),
|
||||
(Point::new(7, 0)..Point::new(8, 4), "⋯"),
|
||||
Point::new(0, 2)..Point::new(1, 2),
|
||||
Point::new(2, 3)..Point::new(4, 1),
|
||||
Point::new(7, 0)..Point::new(8, 4),
|
||||
],
|
||||
true,
|
||||
cx,
|
||||
@@ -3891,9 +3891,9 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) {
|
||||
_ = view.update(cx, |view, cx| {
|
||||
view.fold_ranges(
|
||||
vec![
|
||||
(Point::new(0, 2)..Point::new(1, 2), "⋯"),
|
||||
(Point::new(2, 3)..Point::new(4, 1), "⋯"),
|
||||
(Point::new(7, 0)..Point::new(8, 4), "⋯"),
|
||||
Point::new(0, 2)..Point::new(1, 2),
|
||||
Point::new(2, 3)..Point::new(4, 1),
|
||||
Point::new(7, 0)..Point::new(8, 4),
|
||||
],
|
||||
true,
|
||||
cx,
|
||||
@@ -4548,8 +4548,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||
_ = view.update(cx, |view, cx| {
|
||||
view.fold_ranges(
|
||||
vec![
|
||||
(Point::new(0, 21)..Point::new(0, 24), "⋯"),
|
||||
(Point::new(3, 20)..Point::new(3, 22), "⋯"),
|
||||
Point::new(0, 21)..Point::new(0, 24),
|
||||
Point::new(3, 20)..Point::new(3, 22),
|
||||
],
|
||||
true,
|
||||
cx,
|
||||
@@ -11448,67 +11448,6 @@ async fn test_multiple_expanded_hunks_merge(
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_flap_insertion_and_rendering(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let editor = cx.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
|
||||
build_editor(buffer, cx)
|
||||
});
|
||||
|
||||
let render_args = Arc::new(Mutex::new(None));
|
||||
let snapshot = editor
|
||||
.update(cx, |editor, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let range =
|
||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
|
||||
|
||||
struct RenderArgs {
|
||||
row: MultiBufferRow,
|
||||
folded: bool,
|
||||
callback: Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>,
|
||||
}
|
||||
|
||||
let flap = Flap::new(
|
||||
range,
|
||||
{
|
||||
let toggle_callback = render_args.clone();
|
||||
move |row, folded, callback, _cx| {
|
||||
*toggle_callback.lock() = Some(RenderArgs {
|
||||
row,
|
||||
folded,
|
||||
callback,
|
||||
});
|
||||
div()
|
||||
}
|
||||
},
|
||||
|_row, _folded, _cx| div(),
|
||||
);
|
||||
|
||||
editor.insert_flaps(Some(flap), cx);
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let _div = snapshot.render_fold_toggle(MultiBufferRow(1), false, cx.view().clone(), cx);
|
||||
snapshot
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let render_args = render_args.lock().take().unwrap();
|
||||
assert_eq!(render_args.row, MultiBufferRow(1));
|
||||
assert_eq!(render_args.folded, false);
|
||||
assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
|
||||
|
||||
cx.update_window(*editor, |_, cx| (render_args.callback)(true, cx))
|
||||
.unwrap();
|
||||
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(cx)).unwrap();
|
||||
assert!(snapshot.is_line_folded(MultiBufferRow(1)));
|
||||
|
||||
cx.update_window(*editor, |_, cx| (render_args.callback)(false, cx))
|
||||
.unwrap();
|
||||
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(cx)).unwrap();
|
||||
assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
|
||||
}
|
||||
|
||||
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
|
||||
point..point
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::{
|
||||
blame_entry_tooltip::{blame_entry_relative_timestamp, BlameEntryTooltip},
|
||||
display_map::{
|
||||
BlockContext, BlockStyle, DisplaySnapshot, HighlightedChunk, ToDisplayPoint, TransformBlock,
|
||||
BlockContext, BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, ToDisplayPoint,
|
||||
TransformBlock,
|
||||
},
|
||||
editor_settings::{
|
||||
CurrentLineHighlight, DoubleClickInMultibuffer, MultiCursorModifier, ShowScrollbar,
|
||||
@@ -50,7 +51,7 @@ use smallvec::SmallVec;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
borrow::Cow,
|
||||
cmp::{self, Ordering},
|
||||
cmp::{self, max, Ordering},
|
||||
fmt::Write,
|
||||
iter, mem,
|
||||
ops::{Deref, Range},
|
||||
@@ -383,7 +384,6 @@ impl EditorElement {
|
||||
register_action(view, cx, Editor::unique_lines_case_insensitive);
|
||||
register_action(view, cx, Editor::unique_lines_case_sensitive);
|
||||
register_action(view, cx, Editor::accept_partial_inline_completion);
|
||||
register_action(view, cx, Editor::accept_inline_completion);
|
||||
register_action(view, cx, Editor::revert_selected_hunks);
|
||||
register_action(view, cx, Editor::open_active_item_in_terminal)
|
||||
}
|
||||
@@ -869,11 +869,6 @@ impl EditorElement {
|
||||
snapshot
|
||||
.folds_in_range(visible_anchor_range.clone())
|
||||
.filter_map(|fold| {
|
||||
// Skip folds that have no text.
|
||||
if fold.text.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let fold_range = fold.range.clone();
|
||||
let display_range = fold.range.start.to_display_point(&snapshot)
|
||||
..fold.range.end.to_display_point(&snapshot);
|
||||
@@ -1168,17 +1163,28 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepaint_gutter_fold_toggles(
|
||||
fn layout_gutter_fold_indicators(
|
||||
&self,
|
||||
toggles: &mut [Option<AnyElement>],
|
||||
fold_statuses: Vec<Option<(FoldStatus, MultiBufferRow, bool)>>,
|
||||
line_height: Pixels,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
gutter_settings: crate::editor_settings::Gutter,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_hitbox: &Hitbox,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
for (ix, fold_indicator) in toggles.iter_mut().enumerate() {
|
||||
) -> Vec<Option<AnyElement>> {
|
||||
let mut indicators = self.editor.update(cx, |editor, cx| {
|
||||
editor.render_fold_indicators(
|
||||
fold_statuses,
|
||||
&self.style,
|
||||
editor.gutter_hovered,
|
||||
line_height,
|
||||
gutter_dimensions.margin,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
for (ix, fold_indicator) in indicators.iter_mut().enumerate() {
|
||||
if let Some(fold_indicator) = fold_indicator {
|
||||
debug_assert!(gutter_settings.folds);
|
||||
let available_space = size(
|
||||
@@ -1201,49 +1207,8 @@ impl EditorElement {
|
||||
fold_indicator.prepaint_as_root(origin, available_space, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepaint_flap_trailers(
|
||||
&self,
|
||||
trailers: Vec<Option<AnyElement>>,
|
||||
lines: &[LineWithInvisibles],
|
||||
line_height: Pixels,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
em_width: Pixels,
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<Option<FlapTrailerLayout>> {
|
||||
trailers
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, element)| {
|
||||
let mut element = element?;
|
||||
let available_space = size(
|
||||
AvailableSpace::MinContent,
|
||||
AvailableSpace::Definite(line_height),
|
||||
);
|
||||
let size = element.layout_as_root(available_space, cx);
|
||||
|
||||
let line = &lines[ix].line;
|
||||
let padding = if line.width == Pixels::ZERO {
|
||||
Pixels::ZERO
|
||||
} else {
|
||||
4. * em_width
|
||||
};
|
||||
let position = point(
|
||||
scroll_pixel_position.x + line.width + padding,
|
||||
ix as f32 * line_height - (scroll_pixel_position.y % line_height),
|
||||
);
|
||||
let centering_offset = point(px(0.), (line_height - size.height) / 2.);
|
||||
let origin = content_origin + position + centering_offset;
|
||||
element.prepaint_as_root(origin, available_space, cx);
|
||||
Some(FlapTrailerLayout {
|
||||
element,
|
||||
bounds: Bounds::new(origin, size),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
indicators
|
||||
}
|
||||
|
||||
// Folds contained in a hunk are ignored apart from shrinking visual size
|
||||
@@ -1327,7 +1292,6 @@ impl EditorElement {
|
||||
display_row: DisplayRow,
|
||||
display_snapshot: &DisplaySnapshot,
|
||||
line_layout: &LineWithInvisibles,
|
||||
flap_trailer: Option<&FlapTrailerLayout>,
|
||||
em_width: Pixels,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
@@ -1367,22 +1331,17 @@ impl EditorElement {
|
||||
let start_x = {
|
||||
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.;
|
||||
|
||||
let line_end = if let Some(flap_trailer) = flap_trailer {
|
||||
flap_trailer.bounds.right()
|
||||
} else {
|
||||
content_origin.x - scroll_pixel_position.x + line_layout.line.width
|
||||
};
|
||||
let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS;
|
||||
let padded_line_width =
|
||||
line_layout.line.width + (em_width * INLINE_BLAME_PADDING_EM_WIDTHS);
|
||||
|
||||
let min_column_in_pixels = ProjectSettings::get_global(cx)
|
||||
let min_column = ProjectSettings::get_global(cx)
|
||||
.git
|
||||
.inline_blame
|
||||
.and_then(|settings| settings.min_column)
|
||||
.map(|col| self.column_pixels(col as usize, cx))
|
||||
.unwrap_or(px(0.));
|
||||
let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels;
|
||||
|
||||
cmp::max(padded_line_end, min_start)
|
||||
(content_origin.x - scroll_pixel_position.x) + max(padded_line_width, min_column)
|
||||
};
|
||||
|
||||
let absolute_offset = point(start_x, start_y);
|
||||
@@ -1621,16 +1580,13 @@ impl EditorElement {
|
||||
active_rows: &BTreeMap<DisplayRow, bool>,
|
||||
newest_selection_head: Option<DisplayPoint>,
|
||||
snapshot: &EditorSnapshot,
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<Option<ShapedLine>> {
|
||||
let include_line_numbers = snapshot.show_line_numbers.unwrap_or_else(|| {
|
||||
EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode == EditorMode::Full
|
||||
});
|
||||
if !include_line_numbers {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
cx: &WindowContext,
|
||||
) -> (
|
||||
Vec<Option<ShapedLine>>,
|
||||
Vec<Option<(FoldStatus, MultiBufferRow, bool)>>,
|
||||
) {
|
||||
let editor = self.editor.read(cx);
|
||||
let is_singleton = editor.is_singleton(cx);
|
||||
let newest_selection_head = newest_selection_head.unwrap_or_else(|| {
|
||||
let newest = editor.selections.newest::<Point>(cx);
|
||||
SelectionLayout::new(
|
||||
@@ -1645,100 +1601,69 @@ impl EditorElement {
|
||||
.head
|
||||
});
|
||||
let font_size = self.style.text.font_size.to_pixels(cx.rem_size());
|
||||
|
||||
let include_line_numbers =
|
||||
EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode == EditorMode::Full;
|
||||
let include_fold_statuses =
|
||||
EditorSettings::get_global(cx).gutter.folds && snapshot.mode == EditorMode::Full;
|
||||
let mut shaped_line_numbers = Vec::with_capacity(rows.len());
|
||||
let mut fold_statuses = Vec::with_capacity(rows.len());
|
||||
let mut line_number = String::new();
|
||||
let is_relative = EditorSettings::get_global(cx).relative_line_numbers;
|
||||
let relative_to = if is_relative {
|
||||
Some(newest_selection_head.row())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let relative_rows = self.calculate_relative_line_numbers(snapshot, &rows, relative_to);
|
||||
let mut line_number = String::new();
|
||||
buffer_rows
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, multibuffer_row)| {
|
||||
let multibuffer_row = multibuffer_row?;
|
||||
let display_row = DisplayRow(rows.start.0 + ix as u32);
|
||||
let color = if active_rows.contains_key(&display_row) {
|
||||
cx.theme().colors().editor_active_line_number
|
||||
} else {
|
||||
cx.theme().colors().editor_line_number
|
||||
};
|
||||
line_number.clear();
|
||||
let default_number = multibuffer_row.0 + 1;
|
||||
let number = relative_rows
|
||||
.get(&DisplayRow(ix as u32 + rows.start.0))
|
||||
.unwrap_or(&default_number);
|
||||
write!(&mut line_number, "{number}").unwrap();
|
||||
let run = TextRun {
|
||||
len: line_number.len(),
|
||||
font: self.style.text.font(),
|
||||
color,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
};
|
||||
let shaped_line = cx
|
||||
.text_system()
|
||||
.shape_line(line_number.clone().into(), font_size, &[run])
|
||||
.unwrap();
|
||||
Some(shaped_line)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn layout_gutter_fold_toggles(
|
||||
&self,
|
||||
rows: Range<DisplayRow>,
|
||||
buffer_rows: impl IntoIterator<Item = Option<MultiBufferRow>>,
|
||||
active_rows: &BTreeMap<DisplayRow, bool>,
|
||||
snapshot: &EditorSnapshot,
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<Option<AnyElement>> {
|
||||
let include_fold_statuses = EditorSettings::get_global(cx).gutter.folds
|
||||
&& snapshot.mode == EditorMode::Full
|
||||
&& self.editor.read(cx).is_singleton(cx);
|
||||
if include_fold_statuses {
|
||||
buffer_rows
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, row)| {
|
||||
if let Some(multibuffer_row) = row {
|
||||
let display_row = DisplayRow(rows.start.0 + ix as u32);
|
||||
let active = active_rows.contains_key(&display_row);
|
||||
snapshot.render_fold_toggle(
|
||||
multibuffer_row,
|
||||
active,
|
||||
self.editor.clone(),
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn layout_flap_trailers(
|
||||
&self,
|
||||
buffer_rows: impl IntoIterator<Item = Option<MultiBufferRow>>,
|
||||
snapshot: &EditorSnapshot,
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<Option<AnyElement>> {
|
||||
buffer_rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
if let Some(multibuffer_row) = row {
|
||||
snapshot.render_flap_trailer(multibuffer_row, cx)
|
||||
} else {
|
||||
None
|
||||
for (ix, row) in buffer_rows.into_iter().enumerate() {
|
||||
let display_row = DisplayRow(rows.start.0 + ix as u32);
|
||||
let (active, color) = if active_rows.contains_key(&display_row) {
|
||||
(true, cx.theme().colors().editor_active_line_number)
|
||||
} else {
|
||||
(false, cx.theme().colors().editor_line_number)
|
||||
};
|
||||
if let Some(multibuffer_row) = row {
|
||||
if include_line_numbers {
|
||||
line_number.clear();
|
||||
let default_number = multibuffer_row.0 + 1;
|
||||
let number = relative_rows
|
||||
.get(&DisplayRow(ix as u32 + rows.start.0))
|
||||
.unwrap_or(&default_number);
|
||||
write!(&mut line_number, "{number}").unwrap();
|
||||
let run = TextRun {
|
||||
len: line_number.len(),
|
||||
font: self.style.text.font(),
|
||||
color,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
};
|
||||
let shaped_line = cx
|
||||
.text_system()
|
||||
.shape_line(line_number.clone().into(), font_size, &[run])
|
||||
.unwrap();
|
||||
shaped_line_numbers.push(Some(shaped_line));
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
if include_fold_statuses {
|
||||
fold_statuses.push(
|
||||
is_singleton
|
||||
.then(|| {
|
||||
snapshot
|
||||
.fold_for_line(multibuffer_row)
|
||||
.map(|fold_status| (fold_status, multibuffer_row, active))
|
||||
})
|
||||
.flatten(),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
fold_statuses.push(None);
|
||||
shaped_line_numbers.push(None);
|
||||
}
|
||||
}
|
||||
|
||||
(shaped_line_numbers, fold_statuses)
|
||||
}
|
||||
|
||||
fn layout_lines(
|
||||
@@ -2513,16 +2438,10 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
let show_git_gutter = layout
|
||||
.position_map
|
||||
.snapshot
|
||||
.show_git_diff_gutter
|
||||
.unwrap_or_else(|| {
|
||||
matches!(
|
||||
ProjectSettings::get_global(cx).git.git_gutter,
|
||||
Some(GitGutterSetting::TrackedFiles)
|
||||
)
|
||||
});
|
||||
let show_git_gutter = matches!(
|
||||
ProjectSettings::get_global(cx).git.git_gutter,
|
||||
Some(GitGutterSetting::TrackedFiles)
|
||||
);
|
||||
if show_git_gutter {
|
||||
Self::paint_diff_hunks(layout.gutter_hitbox.bounds, layout, cx)
|
||||
}
|
||||
@@ -2546,8 +2465,8 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
cx.paint_layer(layout.gutter_hitbox.bounds, |cx| {
|
||||
cx.with_element_namespace("gutter_fold_toggles", |cx| {
|
||||
for fold_indicator in layout.gutter_fold_toggles.iter_mut().flatten() {
|
||||
cx.with_element_namespace("gutter_fold_indicators", |cx| {
|
||||
for fold_indicator in layout.fold_indicators.iter_mut().flatten() {
|
||||
fold_indicator.paint(cx);
|
||||
}
|
||||
});
|
||||
@@ -2727,11 +2646,6 @@ impl EditorElement {
|
||||
self.paint_redactions(layout, cx);
|
||||
self.paint_cursors(layout, cx);
|
||||
self.paint_inline_blame(layout, cx);
|
||||
cx.with_element_namespace("flap_trailers", |cx| {
|
||||
for trailer in layout.flap_trailers.iter_mut().flatten() {
|
||||
trailer.element.paint(cx);
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -4078,29 +3992,15 @@ impl Element for EditorElement {
|
||||
cx,
|
||||
);
|
||||
|
||||
let line_numbers = self.layout_line_numbers(
|
||||
let (line_numbers, fold_statuses) = self.layout_line_numbers(
|
||||
start_row..end_row,
|
||||
buffer_rows.iter().copied(),
|
||||
buffer_rows.clone().into_iter(),
|
||||
&active_rows,
|
||||
newest_selection_head,
|
||||
&snapshot,
|
||||
cx,
|
||||
);
|
||||
|
||||
let mut gutter_fold_toggles =
|
||||
cx.with_element_namespace("gutter_fold_toggles", |cx| {
|
||||
self.layout_gutter_fold_toggles(
|
||||
start_row..end_row,
|
||||
buffer_rows.iter().copied(),
|
||||
&active_rows,
|
||||
&snapshot,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let flap_trailers = cx.with_element_namespace("flap_trailers", |cx| {
|
||||
self.layout_flap_trailers(buffer_rows.iter().copied(), &snapshot, cx)
|
||||
});
|
||||
|
||||
let display_hunks = self.layout_git_gutters(
|
||||
line_height,
|
||||
&gutter_hitbox,
|
||||
@@ -4146,30 +4046,15 @@ impl Element for EditorElement {
|
||||
scroll_position.y * line_height,
|
||||
);
|
||||
|
||||
let flap_trailers = cx.with_element_namespace("flap_trailers", |cx| {
|
||||
self.prepaint_flap_trailers(
|
||||
flap_trailers,
|
||||
&line_layouts,
|
||||
line_height,
|
||||
content_origin,
|
||||
scroll_pixel_position,
|
||||
em_width,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let mut inline_blame = None;
|
||||
if let Some(newest_selection_head) = newest_selection_head {
|
||||
let display_row = newest_selection_head.row();
|
||||
if (start_row..end_row).contains(&display_row) {
|
||||
let line_ix = display_row.minus(start_row) as usize;
|
||||
let line_layout = &line_layouts[line_ix];
|
||||
let flap_trailer_layout = flap_trailers[line_ix].as_ref();
|
||||
let line_layout = &line_layouts[display_row.minus(start_row) as usize];
|
||||
inline_blame = self.layout_inline_blame(
|
||||
display_row,
|
||||
&snapshot.display_snapshot,
|
||||
line_layout,
|
||||
flap_trailer_layout,
|
||||
em_width,
|
||||
content_origin,
|
||||
scroll_pixel_position,
|
||||
@@ -4287,11 +4172,7 @@ impl Element for EditorElement {
|
||||
gutter_dimensions.width - gutter_dimensions.left_padding,
|
||||
cx,
|
||||
);
|
||||
|
||||
let show_code_actions = snapshot
|
||||
.show_code_actions
|
||||
.unwrap_or_else(|| gutter_settings.code_actions);
|
||||
if show_code_actions {
|
||||
if gutter_settings.code_actions {
|
||||
let newest_selection_point =
|
||||
newest_selection_head.to_point(&snapshot.display_snapshot);
|
||||
let buffer = snapshot.buffer_snapshot.buffer_line_for_row(
|
||||
@@ -4345,17 +4226,21 @@ impl Element for EditorElement {
|
||||
|
||||
let mouse_context_menu = self.layout_mouse_context_menu(cx);
|
||||
|
||||
cx.with_element_namespace("gutter_fold_toggles", |cx| {
|
||||
self.prepaint_gutter_fold_toggles(
|
||||
&mut gutter_fold_toggles,
|
||||
line_height,
|
||||
&gutter_dimensions,
|
||||
gutter_settings,
|
||||
scroll_pixel_position,
|
||||
&gutter_hitbox,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let fold_indicators = if gutter_settings.folds {
|
||||
cx.with_element_namespace("gutter_fold_indicators", |cx| {
|
||||
self.layout_gutter_fold_indicators(
|
||||
fold_statuses,
|
||||
line_height,
|
||||
&gutter_dimensions,
|
||||
gutter_settings,
|
||||
scroll_pixel_position,
|
||||
&gutter_hitbox,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let invisible_symbol_font_size = font_size / 2.;
|
||||
let tab_invisible = cx
|
||||
@@ -4425,8 +4310,7 @@ impl Element for EditorElement {
|
||||
mouse_context_menu,
|
||||
test_indicators,
|
||||
code_actions_indicator,
|
||||
gutter_fold_toggles,
|
||||
flap_trailers,
|
||||
fold_indicators,
|
||||
tab_invisible,
|
||||
space_invisible,
|
||||
}
|
||||
@@ -4546,8 +4430,7 @@ pub struct EditorLayout {
|
||||
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
|
||||
code_actions_indicator: Option<AnyElement>,
|
||||
test_indicators: Vec<AnyElement>,
|
||||
gutter_fold_toggles: Vec<Option<AnyElement>>,
|
||||
flap_trailers: Vec<Option<FlapTrailerLayout>>,
|
||||
fold_indicators: Vec<Option<AnyElement>>,
|
||||
mouse_context_menu: Option<AnyElement>,
|
||||
tab_invisible: ShapedLine,
|
||||
space_invisible: ShapedLine,
|
||||
@@ -4671,11 +4554,6 @@ impl ScrollbarLayout {
|
||||
}
|
||||
}
|
||||
|
||||
struct FlapTrailerLayout {
|
||||
element: AnyElement,
|
||||
bounds: Bounds<Pixels>,
|
||||
}
|
||||
|
||||
struct FoldLayout {
|
||||
display_range: Range<DisplayPoint>,
|
||||
hover_element: AnyElement,
|
||||
@@ -5094,14 +4972,16 @@ mod tests {
|
||||
|
||||
let layouts = cx
|
||||
.update_window(*window, |_, cx| {
|
||||
element.layout_line_numbers(
|
||||
DisplayRow(0)..DisplayRow(6),
|
||||
(0..6).map(MultiBufferRow).map(Some),
|
||||
&Default::default(),
|
||||
Some(DisplayPoint::new(DisplayRow(0), 0)),
|
||||
&snapshot,
|
||||
cx,
|
||||
)
|
||||
element
|
||||
.layout_line_numbers(
|
||||
DisplayRow(0)..DisplayRow(6),
|
||||
(0..6).map(MultiBufferRow).map(Some),
|
||||
&Default::default(),
|
||||
Some(DisplayPoint::new(DisplayRow(0), 0)),
|
||||
&snapshot,
|
||||
cx,
|
||||
)
|
||||
.0
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(layouts.len(), 6);
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use crate::Editor;
|
||||
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use anyhow::Context;
|
||||
use gpui::{Model, WindowContext};
|
||||
use language::ContextProvider;
|
||||
use project::{BasicContextProvider, Location, Project};
|
||||
use gpui::WindowContext;
|
||||
use language::{BasicContextProvider, ContextProvider};
|
||||
use project::{Location, WorktreeId};
|
||||
use task::{TaskContext, TaskVariables, VariableName};
|
||||
use text::Point;
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) fn task_context_for_location(
|
||||
captured_variables: TaskVariables,
|
||||
workspace: &Workspace,
|
||||
location: Location,
|
||||
cx: &mut WindowContext<'_>,
|
||||
@@ -19,33 +19,44 @@ pub(crate) fn task_context_for_location(
|
||||
.log_err()
|
||||
.flatten();
|
||||
|
||||
let mut task_variables = combine_task_variables(
|
||||
captured_variables,
|
||||
let buffer = location.buffer.clone();
|
||||
let language_context_provider = buffer
|
||||
.read(cx)
|
||||
.language()
|
||||
.and_then(|language| language.context_provider())
|
||||
.unwrap_or_else(|| Arc::new(BasicContextProvider));
|
||||
|
||||
let worktree_abs_path = buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map(|file| WorktreeId::from_usize(file.worktree_id()))
|
||||
.and_then(|worktree_id| {
|
||||
workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
.map(|worktree| worktree.read(cx).abs_path())
|
||||
});
|
||||
let task_variables = combine_task_variables(
|
||||
worktree_abs_path.as_deref(),
|
||||
location,
|
||||
workspace.project().clone(),
|
||||
language_context_provider.as_ref(),
|
||||
cx,
|
||||
)
|
||||
.log_err()?;
|
||||
// Remove all custom entries starting with _, as they're not intended for use by the end user.
|
||||
task_variables.sweep();
|
||||
|
||||
Some(TaskContext {
|
||||
cwd,
|
||||
task_variables,
|
||||
})
|
||||
}
|
||||
|
||||
fn task_context_with_editor(
|
||||
pub(crate) fn task_context_with_editor(
|
||||
workspace: &Workspace,
|
||||
editor: &mut Editor,
|
||||
cx: &mut WindowContext<'_>,
|
||||
) -> Option<TaskContext> {
|
||||
let (selection, buffer, editor_snapshot) = {
|
||||
let mut selection = editor.selections.newest::<Point>(cx);
|
||||
if editor.selections.line_mode {
|
||||
selection.start = Point::new(selection.start.row, 0);
|
||||
selection.end = Point::new(selection.end.row + 1, 0);
|
||||
}
|
||||
let selection = editor.selections.newest::<usize>(cx);
|
||||
let (buffer, _, _) = editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
@@ -68,21 +79,21 @@ fn task_context_with_editor(
|
||||
buffer,
|
||||
range: start..end,
|
||||
};
|
||||
let captured_variables = {
|
||||
let mut variables = TaskVariables::default();
|
||||
task_context_for_location(workspace, location.clone(), cx).map(|mut task_context| {
|
||||
for range in location
|
||||
.buffer
|
||||
.read(cx)
|
||||
.snapshot()
|
||||
.runnable_ranges(location.range.clone())
|
||||
.runnable_ranges(location.range)
|
||||
{
|
||||
for (capture_name, value) in range.extra_captures {
|
||||
variables.insert(VariableName::Custom(capture_name.into()), value);
|
||||
task_context
|
||||
.task_variables
|
||||
.insert(VariableName::Custom(capture_name.into()), value);
|
||||
}
|
||||
}
|
||||
variables
|
||||
};
|
||||
task_context_for_location(captured_variables, workspace, location.clone(), cx)
|
||||
task_context
|
||||
})
|
||||
}
|
||||
|
||||
pub fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContext {
|
||||
@@ -98,26 +109,24 @@ pub fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskCo
|
||||
}
|
||||
|
||||
fn combine_task_variables(
|
||||
mut captured_variables: TaskVariables,
|
||||
worktree_abs_path: Option<&Path>,
|
||||
location: Location,
|
||||
project: Model<Project>,
|
||||
context_provider: &dyn ContextProvider,
|
||||
cx: &mut WindowContext<'_>,
|
||||
) -> anyhow::Result<TaskVariables> {
|
||||
let language_context_provider = location
|
||||
.buffer
|
||||
.read(cx)
|
||||
.language()
|
||||
.and_then(|language| language.context_provider());
|
||||
let baseline = BasicContextProvider::new(project)
|
||||
.build_context(&captured_variables, &location, cx)
|
||||
.context("building basic default context")?;
|
||||
captured_variables.extend(baseline);
|
||||
if let Some(provider) = language_context_provider {
|
||||
captured_variables.extend(
|
||||
provider
|
||||
.build_context(&captured_variables, &location, cx)
|
||||
if context_provider.is_basic() {
|
||||
context_provider
|
||||
.build_context(worktree_abs_path, &location, cx)
|
||||
.context("building basic provider context")
|
||||
} else {
|
||||
let mut basic_context = BasicContextProvider
|
||||
.build_context(worktree_abs_path, &location, cx)
|
||||
.context("building basic default context")?;
|
||||
basic_context.extend(
|
||||
context_provider
|
||||
.build_context(worktree_abs_path, &location, cx)
|
||||
.context("building provider context ")?,
|
||||
);
|
||||
Ok(basic_context)
|
||||
}
|
||||
Ok(captured_variables)
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ log.workspace = true
|
||||
lsp.workspace = true
|
||||
node_runtime.workspace = true
|
||||
project.workspace = true
|
||||
release_channel.workspace = true
|
||||
schemars.workspace = true
|
||||
semantic_version.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -30,11 +30,10 @@ use gpui::{
|
||||
};
|
||||
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use language::{
|
||||
LanguageConfig, LanguageMatcher, LanguageQueries, LanguageRegistry, QUERY_FILENAME_PREFIXES,
|
||||
ContextProviderWithTasks, LanguageConfig, LanguageMatcher, LanguageQueries, LanguageRegistry,
|
||||
QUERY_FILENAME_PREFIXES,
|
||||
};
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::ContextProviderWithTasks;
|
||||
use release_channel::ReleaseChannel;
|
||||
use semantic_version::SemanticVersion;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
@@ -71,10 +70,7 @@ pub fn schema_version_range() -> RangeInclusive<SchemaVersion> {
|
||||
}
|
||||
|
||||
/// Returns whether the given extension version is compatible with this version of Zed.
|
||||
pub fn is_version_compatible(
|
||||
release_channel: ReleaseChannel,
|
||||
extension_version: &ExtensionMetadata,
|
||||
) -> bool {
|
||||
pub fn is_version_compatible(extension_version: &ExtensionMetadata) -> bool {
|
||||
let schema_version = extension_version.manifest.schema_version.unwrap_or(0);
|
||||
if CURRENT_SCHEMA_VERSION.0 < schema_version {
|
||||
return false;
|
||||
@@ -86,7 +82,7 @@ pub fn is_version_compatible(
|
||||
.as_ref()
|
||||
.and_then(|wasm_api_version| SemanticVersion::from_str(wasm_api_version).ok())
|
||||
{
|
||||
if !is_supported_wasm_api_version(release_channel, wasm_api_version) {
|
||||
if !is_supported_wasm_api_version(wasm_api_version) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -432,7 +428,7 @@ impl ExtensionStore {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<ExtensionMetadata>>> {
|
||||
let schema_versions = schema_version_range();
|
||||
let wasm_api_versions = wasm_api_version_range(ReleaseChannel::global(cx));
|
||||
let wasm_api_versions = wasm_api_version_range();
|
||||
let extension_settings = ExtensionSettings::get_global(cx);
|
||||
let extension_ids = self
|
||||
.extension_index
|
||||
@@ -685,7 +681,7 @@ impl ExtensionStore {
|
||||
log::info!("installing extension {extension_id} latest version");
|
||||
|
||||
let schema_versions = schema_version_range();
|
||||
let wasm_api_versions = wasm_api_version_range(ReleaseChannel::global(cx));
|
||||
let wasm_api_versions = wasm_api_version_range();
|
||||
|
||||
let Some(url) = self
|
||||
.http_client
|
||||
|
||||
@@ -714,7 +714,6 @@ fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
release_channel::init("0.0.0", cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
Project::init_settings(cx);
|
||||
ExtensionSettings::register(cx);
|
||||
|
||||
@@ -16,7 +16,6 @@ use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task};
|
||||
use http::HttpClient;
|
||||
use language::LanguageRegistry;
|
||||
use node_runtime::NodeRuntime;
|
||||
use release_channel::ReleaseChannel;
|
||||
use semantic_version::SemanticVersion;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
@@ -31,7 +30,6 @@ use wit::Extension;
|
||||
|
||||
pub(crate) struct WasmHost {
|
||||
engine: Engine,
|
||||
release_channel: ReleaseChannel,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
node_runtime: Arc<dyn NodeRuntime>,
|
||||
pub(crate) language_registry: Arc<LanguageRegistry>,
|
||||
@@ -98,7 +96,6 @@ impl WasmHost {
|
||||
http_client,
|
||||
node_runtime,
|
||||
language_registry,
|
||||
release_channel: ReleaseChannel::global(cx),
|
||||
_main_thread_message_task: task,
|
||||
main_thread_message_tx: tx,
|
||||
})
|
||||
@@ -127,13 +124,8 @@ impl WasmHost {
|
||||
},
|
||||
);
|
||||
|
||||
let (mut extension, instance) = Extension::instantiate_async(
|
||||
&mut store,
|
||||
this.release_channel,
|
||||
zed_api_version,
|
||||
&component,
|
||||
)
|
||||
.await?;
|
||||
let (mut extension, instance) =
|
||||
Extension::instantiate_async(&mut store, zed_api_version, &component).await?;
|
||||
|
||||
extension
|
||||
.call_init_extension(&mut store)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
mod since_v0_0_1;
|
||||
mod since_v0_0_4;
|
||||
mod since_v0_0_6;
|
||||
mod since_v0_0_7;
|
||||
use release_channel::ReleaseChannel;
|
||||
use since_v0_0_7 as latest;
|
||||
use since_v0_0_6 as latest;
|
||||
|
||||
use super::{wasm_engine, WasmState};
|
||||
use anyhow::{Context, Result};
|
||||
@@ -37,27 +35,17 @@ fn wasi_view(state: &mut WasmState) -> &mut WasmState {
|
||||
}
|
||||
|
||||
/// Returns whether the given Wasm API version is supported by the Wasm host.
|
||||
pub fn is_supported_wasm_api_version(
|
||||
release_channel: ReleaseChannel,
|
||||
version: SemanticVersion,
|
||||
) -> bool {
|
||||
wasm_api_version_range(release_channel).contains(&version)
|
||||
pub fn is_supported_wasm_api_version(version: SemanticVersion) -> bool {
|
||||
wasm_api_version_range().contains(&version)
|
||||
}
|
||||
|
||||
/// Returns the Wasm API version range that is supported by the Wasm host.
|
||||
#[inline(always)]
|
||||
pub fn wasm_api_version_range(release_channel: ReleaseChannel) -> RangeInclusive<SemanticVersion> {
|
||||
let max_version = if release_channel == ReleaseChannel::Dev {
|
||||
latest::MAX_VERSION
|
||||
} else {
|
||||
since_v0_0_6::MAX_VERSION
|
||||
};
|
||||
|
||||
since_v0_0_1::MIN_VERSION..=max_version
|
||||
pub fn wasm_api_version_range() -> RangeInclusive<SemanticVersion> {
|
||||
since_v0_0_1::MIN_VERSION..=latest::MAX_VERSION
|
||||
}
|
||||
|
||||
pub enum Extension {
|
||||
V007(since_v0_0_7::Extension),
|
||||
V006(since_v0_0_6::Extension),
|
||||
V004(since_v0_0_4::Extension),
|
||||
V001(since_v0_0_1::Extension),
|
||||
@@ -66,24 +54,14 @@ pub enum Extension {
|
||||
impl Extension {
|
||||
pub async fn instantiate_async(
|
||||
store: &mut Store<WasmState>,
|
||||
release_channel: ReleaseChannel,
|
||||
version: SemanticVersion,
|
||||
component: &Component,
|
||||
) -> Result<(Self, Instance)> {
|
||||
if release_channel == ReleaseChannel::Dev && version >= latest::MIN_VERSION {
|
||||
if version >= latest::MIN_VERSION {
|
||||
let (extension, instance) =
|
||||
latest::Extension::instantiate_async(store, &component, latest::linker())
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Ok((Self::V007(extension), instance))
|
||||
} else if version >= since_v0_0_6::MIN_VERSION {
|
||||
let (extension, instance) = since_v0_0_6::Extension::instantiate_async(
|
||||
store,
|
||||
&component,
|
||||
since_v0_0_6::linker(),
|
||||
)
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Ok((Self::V006(extension), instance))
|
||||
} else if version >= since_v0_0_4::MIN_VERSION {
|
||||
let (extension, instance) = since_v0_0_4::Extension::instantiate_async(
|
||||
@@ -108,7 +86,6 @@ impl Extension {
|
||||
|
||||
pub async fn call_init_extension(&self, store: &mut Store<WasmState>) -> Result<()> {
|
||||
match self {
|
||||
Extension::V007(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V006(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V004(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V001(ext) => ext.call_init_extension(store).await,
|
||||
@@ -123,14 +100,10 @@ impl Extension {
|
||||
resource: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
) -> Result<Result<Command, String>> {
|
||||
match self {
|
||||
Extension::V007(ext) => {
|
||||
Extension::V006(ext) => {
|
||||
ext.call_language_server_command(store, &language_server_id.0, resource)
|
||||
.await
|
||||
}
|
||||
Extension::V006(ext) => Ok(ext
|
||||
.call_language_server_command(store, &language_server_id.0, resource)
|
||||
.await?
|
||||
.map(|command| command.into())),
|
||||
Extension::V004(ext) => Ok(ext
|
||||
.call_language_server_command(store, config, resource)
|
||||
.await?
|
||||
@@ -150,14 +123,6 @@ impl Extension {
|
||||
resource: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
) -> Result<Result<Option<String>, String>> {
|
||||
match self {
|
||||
Extension::V007(ext) => {
|
||||
ext.call_language_server_initialization_options(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
resource,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V006(ext) => {
|
||||
ext.call_language_server_initialization_options(
|
||||
store,
|
||||
@@ -188,14 +153,6 @@ impl Extension {
|
||||
resource: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
) -> Result<Result<Option<String>, String>> {
|
||||
match self {
|
||||
Extension::V007(ext) => {
|
||||
ext.call_language_server_workspace_configuration(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
resource,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V006(ext) => {
|
||||
ext.call_language_server_workspace_configuration(
|
||||
store,
|
||||
@@ -215,20 +172,11 @@ impl Extension {
|
||||
completions: Vec<latest::Completion>,
|
||||
) -> Result<Result<Vec<Option<CodeLabel>>, String>> {
|
||||
match self {
|
||||
Extension::V007(ext) => {
|
||||
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
|
||||
Extension::V006(ext) => {
|
||||
ext.call_labels_for_completions(store, &language_server_id.0, &completions)
|
||||
.await
|
||||
}
|
||||
Extension::V006(ext) => Ok(ext
|
||||
.call_labels_for_completions(store, &language_server_id.0, &completions)
|
||||
.await?
|
||||
.map(|labels| {
|
||||
labels
|
||||
.into_iter()
|
||||
.map(|label| label.map(Into::into))
|
||||
.collect()
|
||||
})),
|
||||
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,20 +187,11 @@ impl Extension {
|
||||
symbols: Vec<latest::Symbol>,
|
||||
) -> Result<Result<Vec<Option<CodeLabel>>, String>> {
|
||||
match self {
|
||||
Extension::V007(ext) => {
|
||||
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
|
||||
Extension::V006(ext) => {
|
||||
ext.call_labels_for_symbols(store, &language_server_id.0, &symbols)
|
||||
.await
|
||||
}
|
||||
Extension::V006(ext) => Ok(ext
|
||||
.call_labels_for_symbols(store, &language_server_id.0, &symbols)
|
||||
.await?
|
||||
.map(|labels| {
|
||||
labels
|
||||
.into_iter()
|
||||
.map(|label| label.map(Into::into))
|
||||
.collect()
|
||||
})),
|
||||
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
use super::latest;
|
||||
use crate::wasm_host::WasmState;
|
||||
use anyhow::Result;
|
||||
use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
|
||||
use ::settings::Settings;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use async_trait::async_trait;
|
||||
use language::LspAdapterDelegate;
|
||||
use futures::{io::BufReader, FutureExt as _};
|
||||
use language::{
|
||||
language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate,
|
||||
};
|
||||
use project::project_settings::ProjectSettings;
|
||||
use semantic_version::SemanticVersion;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
use util::maybe;
|
||||
use wasmtime::component::{Linker, Resource};
|
||||
|
||||
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 6);
|
||||
@@ -15,13 +26,11 @@ wasmtime::component::bindgen!({
|
||||
path: "../extension_api/wit/since_v0.0.6",
|
||||
with: {
|
||||
"worktree": ExtensionWorktree,
|
||||
"zed:extension/github": latest::zed::extension::github,
|
||||
"zed:extension/lsp": latest::zed::extension::lsp,
|
||||
"zed:extension/nodejs": latest::zed::extension::nodejs,
|
||||
"zed:extension/platform": latest::zed::extension::platform,
|
||||
},
|
||||
});
|
||||
|
||||
pub use self::zed::extension::*;
|
||||
|
||||
mod settings {
|
||||
include!("../../../../extension_api/wit/since_v0.0.6/settings.rs");
|
||||
}
|
||||
@@ -30,93 +39,7 @@ pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
|
||||
|
||||
pub fn linker() -> &'static Linker<WasmState> {
|
||||
static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
|
||||
LINKER.get_or_init(|| {
|
||||
super::new_linker(|linker, f| {
|
||||
Extension::add_to_linker(linker, f)?;
|
||||
latest::zed::extension::github::add_to_linker(linker, f)?;
|
||||
latest::zed::extension::nodejs::add_to_linker(linker, f)?;
|
||||
latest::zed::extension::platform::add_to_linker(linker, f)?;
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
impl From<Command> for latest::Command {
|
||||
fn from(value: Command) -> Self {
|
||||
Self {
|
||||
command: value.command,
|
||||
args: value.args,
|
||||
env: value.env,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SettingsLocation> for latest::SettingsLocation {
|
||||
fn from(value: SettingsLocation) -> Self {
|
||||
Self {
|
||||
worktree_id: value.worktree_id,
|
||||
path: value.path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LanguageServerInstallationStatus> for latest::LanguageServerInstallationStatus {
|
||||
fn from(value: LanguageServerInstallationStatus) -> Self {
|
||||
match value {
|
||||
LanguageServerInstallationStatus::None => Self::None,
|
||||
LanguageServerInstallationStatus::Downloading => Self::Downloading,
|
||||
LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate,
|
||||
LanguageServerInstallationStatus::Failed(message) => Self::Failed(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DownloadedFileType> for latest::DownloadedFileType {
|
||||
fn from(value: DownloadedFileType) -> Self {
|
||||
match value {
|
||||
DownloadedFileType::Gzip => Self::Gzip,
|
||||
DownloadedFileType::GzipTar => Self::GzipTar,
|
||||
DownloadedFileType::Zip => Self::Zip,
|
||||
DownloadedFileType::Uncompressed => Self::Uncompressed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Range> for latest::Range {
|
||||
fn from(value: Range) -> Self {
|
||||
Self {
|
||||
start: value.start,
|
||||
end: value.end,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CodeLabelSpan> for latest::CodeLabelSpan {
|
||||
fn from(value: CodeLabelSpan) -> Self {
|
||||
match value {
|
||||
CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()),
|
||||
CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CodeLabelSpanLiteral> for latest::CodeLabelSpanLiteral {
|
||||
fn from(value: CodeLabelSpanLiteral) -> Self {
|
||||
Self {
|
||||
text: value.text,
|
||||
highlight_name: value.highlight_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CodeLabel> for latest::CodeLabel {
|
||||
fn from(value: CodeLabel) -> Self {
|
||||
Self {
|
||||
code: value.code,
|
||||
spans: value.spans.into_iter().map(Into::into).collect(),
|
||||
filter_range: value.filter_range.into(),
|
||||
}
|
||||
}
|
||||
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -125,14 +48,16 @@ impl HostWorktree for WasmState {
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
) -> wasmtime::Result<u64> {
|
||||
latest::HostWorktree::id(self, delegate).await
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate.worktree_id())
|
||||
}
|
||||
|
||||
async fn root_path(
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
) -> wasmtime::Result<String> {
|
||||
latest::HostWorktree::root_path(self, delegate).await
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate.worktree_root_path().to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
async fn read_text_file(
|
||||
@@ -140,14 +65,19 @@ impl HostWorktree for WasmState {
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
path: String,
|
||||
) -> wasmtime::Result<Result<String, String>> {
|
||||
latest::HostWorktree::read_text_file(self, delegate, path).await
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate
|
||||
.read_text_file(path.into())
|
||||
.await
|
||||
.map_err(|error| error.to_string()))
|
||||
}
|
||||
|
||||
async fn shell_env(
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
) -> wasmtime::Result<EnvVars> {
|
||||
latest::HostWorktree::shell_env(self, delegate).await
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate.shell_env().await.into_iter().collect())
|
||||
}
|
||||
|
||||
async fn which(
|
||||
@@ -155,7 +85,11 @@ impl HostWorktree for WasmState {
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
binary_name: String,
|
||||
) -> wasmtime::Result<Option<String>> {
|
||||
latest::HostWorktree::which(self, delegate, binary_name).await
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate
|
||||
.which(binary_name.as_ref())
|
||||
.await
|
||||
.map(|path| path.to_string_lossy().to_string()))
|
||||
}
|
||||
|
||||
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
|
||||
@@ -164,6 +98,107 @@ impl HostWorktree for WasmState {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl nodejs::Host for WasmState {
|
||||
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
|
||||
self.host
|
||||
.node_runtime
|
||||
.binary_path()
|
||||
.await
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
|
||||
async fn npm_package_latest_version(
|
||||
&mut self,
|
||||
package_name: String,
|
||||
) -> wasmtime::Result<Result<String, String>> {
|
||||
self.host
|
||||
.node_runtime
|
||||
.npm_package_latest_version(&package_name)
|
||||
.await
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
|
||||
async fn npm_package_installed_version(
|
||||
&mut self,
|
||||
package_name: String,
|
||||
) -> wasmtime::Result<Result<Option<String>, String>> {
|
||||
self.host
|
||||
.node_runtime
|
||||
.npm_package_installed_version(&self.work_dir(), &package_name)
|
||||
.await
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
|
||||
async fn npm_install_package(
|
||||
&mut self,
|
||||
package_name: String,
|
||||
version: String,
|
||||
) -> wasmtime::Result<Result<(), String>> {
|
||||
self.host
|
||||
.node_runtime
|
||||
.npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
|
||||
.await
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl lsp::Host for WasmState {}
|
||||
|
||||
#[async_trait]
|
||||
impl github::Host for WasmState {
|
||||
async fn latest_github_release(
|
||||
&mut self,
|
||||
repo: String,
|
||||
options: github::GithubReleaseOptions,
|
||||
) -> wasmtime::Result<Result<github::GithubRelease, String>> {
|
||||
maybe!(async {
|
||||
let release = http::github::latest_github_release(
|
||||
&repo,
|
||||
options.require_assets,
|
||||
options.pre_release,
|
||||
self.host.http_client.clone(),
|
||||
)
|
||||
.await?;
|
||||
Ok(github::GithubRelease {
|
||||
version: release.tag_name,
|
||||
assets: release
|
||||
.assets
|
||||
.into_iter()
|
||||
.map(|asset| github::GithubReleaseAsset {
|
||||
name: asset.name,
|
||||
download_url: asset.browser_download_url,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl platform::Host for WasmState {
|
||||
async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> {
|
||||
Ok((
|
||||
match env::consts::OS {
|
||||
"macos" => platform::Os::Mac,
|
||||
"linux" => platform::Os::Linux,
|
||||
"windows" => platform::Os::Windows,
|
||||
_ => panic!("unsupported os"),
|
||||
},
|
||||
match env::consts::ARCH {
|
||||
"aarch64" => platform::Architecture::Aarch64,
|
||||
"x86" => platform::Architecture::X86,
|
||||
"x86_64" => platform::Architecture::X8664,
|
||||
_ => panic!("unsupported architecture"),
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExtensionImports for WasmState {
|
||||
async fn get_settings(
|
||||
@@ -172,13 +207,50 @@ impl ExtensionImports for WasmState {
|
||||
category: String,
|
||||
key: Option<String>,
|
||||
) -> wasmtime::Result<Result<String, String>> {
|
||||
latest::ExtensionImports::get_settings(
|
||||
self,
|
||||
location.map(|location| location.into()),
|
||||
category,
|
||||
key,
|
||||
)
|
||||
.await
|
||||
self.on_main_thread(|cx| {
|
||||
async move {
|
||||
let location = location
|
||||
.as_ref()
|
||||
.map(|location| ::settings::SettingsLocation {
|
||||
worktree_id: location.worktree_id as usize,
|
||||
path: Path::new(&location.path),
|
||||
});
|
||||
|
||||
cx.update(|cx| match category.as_str() {
|
||||
"language" => {
|
||||
let settings =
|
||||
AllLanguageSettings::get(location, cx).language(key.as_deref());
|
||||
Ok(serde_json::to_string(&settings::LanguageSettings {
|
||||
tab_size: settings.tab_size,
|
||||
})?)
|
||||
}
|
||||
"lsp" => {
|
||||
let settings = key
|
||||
.and_then(|key| {
|
||||
ProjectSettings::get(location, cx)
|
||||
.lsp
|
||||
.get(&Arc::<str>::from(key))
|
||||
})
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
Ok(serde_json::to_string(&settings::LspSettings {
|
||||
binary: settings.binary.map(|binary| settings::BinarySettings {
|
||||
path: binary.path,
|
||||
arguments: binary.arguments,
|
||||
}),
|
||||
settings: settings.settings,
|
||||
initialization_options: settings.initialization_options,
|
||||
})?)
|
||||
}
|
||||
_ => {
|
||||
bail!("Unknown settings category: {}", category);
|
||||
}
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
})
|
||||
.await?
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
|
||||
async fn set_language_server_installation_status(
|
||||
@@ -186,12 +258,23 @@ impl ExtensionImports for WasmState {
|
||||
server_name: String,
|
||||
status: LanguageServerInstallationStatus,
|
||||
) -> wasmtime::Result<()> {
|
||||
latest::ExtensionImports::set_language_server_installation_status(
|
||||
self,
|
||||
server_name,
|
||||
status.into(),
|
||||
)
|
||||
.await
|
||||
let status = match status {
|
||||
LanguageServerInstallationStatus::CheckingForUpdate => {
|
||||
LanguageServerBinaryStatus::CheckingForUpdate
|
||||
}
|
||||
LanguageServerInstallationStatus::Downloading => {
|
||||
LanguageServerBinaryStatus::Downloading
|
||||
}
|
||||
LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
|
||||
LanguageServerInstallationStatus::Failed(error) => {
|
||||
LanguageServerBinaryStatus::Failed { error }
|
||||
}
|
||||
};
|
||||
|
||||
self.host
|
||||
.language_registry
|
||||
.update_lsp_status(language::LanguageServerName(server_name.into()), status);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download_file(
|
||||
@@ -200,10 +283,103 @@ impl ExtensionImports for WasmState {
|
||||
path: String,
|
||||
file_type: DownloadedFileType,
|
||||
) -> wasmtime::Result<Result<(), String>> {
|
||||
latest::ExtensionImports::download_file(self, url, path, file_type.into()).await
|
||||
maybe!(async {
|
||||
let path = PathBuf::from(path);
|
||||
let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
|
||||
|
||||
self.host.fs.create_dir(&extension_work_dir).await?;
|
||||
|
||||
let destination_path = self
|
||||
.host
|
||||
.writeable_path_from_extension(&self.manifest.id, &path)?;
|
||||
|
||||
let mut response = self
|
||||
.host
|
||||
.http_client
|
||||
.get(&url, Default::default(), true)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error downloading release: {}", err))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
Err(anyhow!(
|
||||
"download failed with status {}",
|
||||
response.status().to_string()
|
||||
))?;
|
||||
}
|
||||
let body = BufReader::new(response.body_mut());
|
||||
|
||||
match file_type {
|
||||
DownloadedFileType::Uncompressed => {
|
||||
futures::pin_mut!(body);
|
||||
self.host
|
||||
.fs
|
||||
.create_file_with(&destination_path, body)
|
||||
.await?;
|
||||
}
|
||||
DownloadedFileType::Gzip => {
|
||||
let body = GzipDecoder::new(body);
|
||||
futures::pin_mut!(body);
|
||||
self.host
|
||||
.fs
|
||||
.create_file_with(&destination_path, body)
|
||||
.await?;
|
||||
}
|
||||
DownloadedFileType::GzipTar => {
|
||||
let body = GzipDecoder::new(body);
|
||||
futures::pin_mut!(body);
|
||||
self.host
|
||||
.fs
|
||||
.extract_tar_file(&destination_path, Archive::new(body))
|
||||
.await?;
|
||||
}
|
||||
DownloadedFileType::Zip => {
|
||||
let file_name = destination_path
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("invalid download path"))?
|
||||
.to_string_lossy();
|
||||
let zip_filename = format!("{file_name}.zip");
|
||||
let mut zip_path = destination_path.clone();
|
||||
zip_path.set_file_name(zip_filename);
|
||||
|
||||
futures::pin_mut!(body);
|
||||
self.host.fs.create_file_with(&zip_path, body).await?;
|
||||
|
||||
let unzip_status = std::process::Command::new("unzip")
|
||||
.current_dir(&extension_work_dir)
|
||||
.arg("-d")
|
||||
.arg(&destination_path)
|
||||
.arg(&zip_path)
|
||||
.output()?
|
||||
.status;
|
||||
if !unzip_status.success() {
|
||||
Err(anyhow!("failed to unzip {} archive", path.display()))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
|
||||
async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
|
||||
latest::ExtensionImports::make_file_executable(self, path).await
|
||||
#[allow(unused)]
|
||||
let path = self
|
||||
.host
|
||||
.writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::fs::{self, Permissions};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
return fs::set_permissions(&path, Permissions::from_mode(0o755))
|
||||
.map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
|
||||
.to_wasmtime_result();
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
Ok(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,408 +0,0 @@
|
||||
use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
|
||||
use ::settings::Settings;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use async_trait::async_trait;
|
||||
use futures::{io::BufReader, FutureExt as _};
|
||||
use language::{
|
||||
language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate,
|
||||
};
|
||||
use project::project_settings::ProjectSettings;
|
||||
use semantic_version::SemanticVersion;
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
use util::maybe;
|
||||
use wasmtime::component::{Linker, Resource};
|
||||
|
||||
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 7);
|
||||
pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 7);
|
||||
|
||||
wasmtime::component::bindgen!({
|
||||
async: true,
|
||||
path: "../extension_api/wit/since_v0.0.7",
|
||||
with: {
|
||||
"worktree": ExtensionWorktree,
|
||||
},
|
||||
});
|
||||
|
||||
pub use self::zed::extension::*;
|
||||
|
||||
mod settings {
|
||||
include!("../../../../extension_api/wit/since_v0.0.7/settings.rs");
|
||||
}
|
||||
|
||||
pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
|
||||
|
||||
pub fn linker() -> &'static Linker<WasmState> {
|
||||
static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
|
||||
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HostWorktree for WasmState {
|
||||
async fn id(
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
) -> wasmtime::Result<u64> {
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate.worktree_id())
|
||||
}
|
||||
|
||||
async fn root_path(
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
) -> wasmtime::Result<String> {
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate.worktree_root_path().to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
async fn read_text_file(
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
path: String,
|
||||
) -> wasmtime::Result<Result<String, String>> {
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate
|
||||
.read_text_file(path.into())
|
||||
.await
|
||||
.map_err(|error| error.to_string()))
|
||||
}
|
||||
|
||||
async fn shell_env(
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
) -> wasmtime::Result<EnvVars> {
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate.shell_env().await.into_iter().collect())
|
||||
}
|
||||
|
||||
async fn which(
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
binary_name: String,
|
||||
) -> wasmtime::Result<Option<String>> {
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate
|
||||
.which(binary_name.as_ref())
|
||||
.await
|
||||
.map(|path| path.to_string_lossy().to_string()))
|
||||
}
|
||||
|
||||
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
|
||||
// We only ever hand out borrows of worktrees.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl nodejs::Host for WasmState {
|
||||
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
|
||||
self.host
|
||||
.node_runtime
|
||||
.binary_path()
|
||||
.await
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
|
||||
async fn npm_package_latest_version(
|
||||
&mut self,
|
||||
package_name: String,
|
||||
) -> wasmtime::Result<Result<String, String>> {
|
||||
self.host
|
||||
.node_runtime
|
||||
.npm_package_latest_version(&package_name)
|
||||
.await
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
|
||||
async fn npm_package_installed_version(
|
||||
&mut self,
|
||||
package_name: String,
|
||||
) -> wasmtime::Result<Result<Option<String>, String>> {
|
||||
self.host
|
||||
.node_runtime
|
||||
.npm_package_installed_version(&self.work_dir(), &package_name)
|
||||
.await
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
|
||||
async fn npm_install_package(
|
||||
&mut self,
|
||||
package_name: String,
|
||||
version: String,
|
||||
) -> wasmtime::Result<Result<(), String>> {
|
||||
self.host
|
||||
.node_runtime
|
||||
.npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
|
||||
.await
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl lsp::Host for WasmState {}
|
||||
|
||||
impl From<http::github::GithubRelease> for github::GithubRelease {
|
||||
fn from(value: http::github::GithubRelease) -> Self {
|
||||
Self {
|
||||
version: value.tag_name,
|
||||
assets: value.assets.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<http::github::GithubReleaseAsset> for github::GithubReleaseAsset {
|
||||
fn from(value: http::github::GithubReleaseAsset) -> Self {
|
||||
Self {
|
||||
name: value.name,
|
||||
download_url: value.browser_download_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl github::Host for WasmState {
|
||||
async fn latest_github_release(
|
||||
&mut self,
|
||||
repo: String,
|
||||
options: github::GithubReleaseOptions,
|
||||
) -> wasmtime::Result<Result<github::GithubRelease, String>> {
|
||||
maybe!(async {
|
||||
let release = http::github::latest_github_release(
|
||||
&repo,
|
||||
options.require_assets,
|
||||
options.pre_release,
|
||||
self.host.http_client.clone(),
|
||||
)
|
||||
.await?;
|
||||
Ok(release.into())
|
||||
})
|
||||
.await
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
|
||||
async fn github_release_by_tag_name(
|
||||
&mut self,
|
||||
repo: String,
|
||||
tag: String,
|
||||
) -> wasmtime::Result<Result<github::GithubRelease, String>> {
|
||||
maybe!(async {
|
||||
let release =
|
||||
http::github::get_release_by_tag_name(&repo, &tag, self.host.http_client.clone())
|
||||
.await?;
|
||||
Ok(release.into())
|
||||
})
|
||||
.await
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl platform::Host for WasmState {
|
||||
async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> {
|
||||
Ok((
|
||||
match env::consts::OS {
|
||||
"macos" => platform::Os::Mac,
|
||||
"linux" => platform::Os::Linux,
|
||||
"windows" => platform::Os::Windows,
|
||||
_ => panic!("unsupported os"),
|
||||
},
|
||||
match env::consts::ARCH {
|
||||
"aarch64" => platform::Architecture::Aarch64,
|
||||
"x86" => platform::Architecture::X86,
|
||||
"x86_64" => platform::Architecture::X8664,
|
||||
_ => panic!("unsupported architecture"),
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExtensionImports for WasmState {
|
||||
async fn get_settings(
|
||||
&mut self,
|
||||
location: Option<self::SettingsLocation>,
|
||||
category: String,
|
||||
key: Option<String>,
|
||||
) -> wasmtime::Result<Result<String, String>> {
|
||||
self.on_main_thread(|cx| {
|
||||
async move {
|
||||
let location = location
|
||||
.as_ref()
|
||||
.map(|location| ::settings::SettingsLocation {
|
||||
worktree_id: location.worktree_id as usize,
|
||||
path: Path::new(&location.path),
|
||||
});
|
||||
|
||||
cx.update(|cx| match category.as_str() {
|
||||
"language" => {
|
||||
let settings =
|
||||
AllLanguageSettings::get(location, cx).language(key.as_deref());
|
||||
Ok(serde_json::to_string(&settings::LanguageSettings {
|
||||
tab_size: settings.tab_size,
|
||||
})?)
|
||||
}
|
||||
"lsp" => {
|
||||
let settings = key
|
||||
.and_then(|key| {
|
||||
ProjectSettings::get(location, cx)
|
||||
.lsp
|
||||
.get(&Arc::<str>::from(key))
|
||||
})
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
Ok(serde_json::to_string(&settings::LspSettings {
|
||||
binary: settings.binary.map(|binary| settings::BinarySettings {
|
||||
path: binary.path,
|
||||
arguments: binary.arguments,
|
||||
}),
|
||||
settings: settings.settings,
|
||||
initialization_options: settings.initialization_options,
|
||||
})?)
|
||||
}
|
||||
_ => {
|
||||
bail!("Unknown settings category: {}", category);
|
||||
}
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
})
|
||||
.await?
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
|
||||
async fn set_language_server_installation_status(
|
||||
&mut self,
|
||||
server_name: String,
|
||||
status: LanguageServerInstallationStatus,
|
||||
) -> wasmtime::Result<()> {
|
||||
let status = match status {
|
||||
LanguageServerInstallationStatus::CheckingForUpdate => {
|
||||
LanguageServerBinaryStatus::CheckingForUpdate
|
||||
}
|
||||
LanguageServerInstallationStatus::Downloading => {
|
||||
LanguageServerBinaryStatus::Downloading
|
||||
}
|
||||
LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
|
||||
LanguageServerInstallationStatus::Failed(error) => {
|
||||
LanguageServerBinaryStatus::Failed { error }
|
||||
}
|
||||
};
|
||||
|
||||
self.host
|
||||
.language_registry
|
||||
.update_lsp_status(language::LanguageServerName(server_name.into()), status);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download_file(
|
||||
&mut self,
|
||||
url: String,
|
||||
path: String,
|
||||
file_type: DownloadedFileType,
|
||||
) -> wasmtime::Result<Result<(), String>> {
|
||||
maybe!(async {
|
||||
let path = PathBuf::from(path);
|
||||
let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
|
||||
|
||||
self.host.fs.create_dir(&extension_work_dir).await?;
|
||||
|
||||
let destination_path = self
|
||||
.host
|
||||
.writeable_path_from_extension(&self.manifest.id, &path)?;
|
||||
|
||||
let mut response = self
|
||||
.host
|
||||
.http_client
|
||||
.get(&url, Default::default(), true)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error downloading release: {}", err))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
Err(anyhow!(
|
||||
"download failed with status {}",
|
||||
response.status().to_string()
|
||||
))?;
|
||||
}
|
||||
let body = BufReader::new(response.body_mut());
|
||||
|
||||
match file_type {
|
||||
DownloadedFileType::Uncompressed => {
|
||||
futures::pin_mut!(body);
|
||||
self.host
|
||||
.fs
|
||||
.create_file_with(&destination_path, body)
|
||||
.await?;
|
||||
}
|
||||
DownloadedFileType::Gzip => {
|
||||
let body = GzipDecoder::new(body);
|
||||
futures::pin_mut!(body);
|
||||
self.host
|
||||
.fs
|
||||
.create_file_with(&destination_path, body)
|
||||
.await?;
|
||||
}
|
||||
DownloadedFileType::GzipTar => {
|
||||
let body = GzipDecoder::new(body);
|
||||
futures::pin_mut!(body);
|
||||
self.host
|
||||
.fs
|
||||
.extract_tar_file(&destination_path, Archive::new(body))
|
||||
.await?;
|
||||
}
|
||||
DownloadedFileType::Zip => {
|
||||
let file_name = destination_path
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("invalid download path"))?
|
||||
.to_string_lossy();
|
||||
let zip_filename = format!("{file_name}.zip");
|
||||
let mut zip_path = destination_path.clone();
|
||||
zip_path.set_file_name(zip_filename);
|
||||
|
||||
futures::pin_mut!(body);
|
||||
self.host.fs.create_file_with(&zip_path, body).await?;
|
||||
|
||||
let unzip_status = std::process::Command::new("unzip")
|
||||
.current_dir(&extension_work_dir)
|
||||
.arg("-d")
|
||||
.arg(&destination_path)
|
||||
.arg(&zip_path)
|
||||
.output()?
|
||||
.status;
|
||||
if !unzip_status.success() {
|
||||
Err(anyhow!("failed to unzip {} archive", path.display()))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
|
||||
async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
|
||||
#[allow(unused)]
|
||||
let path = self
|
||||
.host
|
||||
.writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::fs::{self, Permissions};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
return fs::set_permissions(&path, Permissions::from_mode(0o755))
|
||||
.map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
|
||||
.to_wasmtime_result();
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
Ok(Ok(()))
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "zed_extension_api"
|
||||
version = "0.0.7"
|
||||
version = "0.0.6"
|
||||
description = "APIs for creating Zed extensions in Rust"
|
||||
repository = "https://github.com/zed-industries/zed"
|
||||
documentation = "https://docs.rs/zed_extension_api"
|
||||
|
||||
@@ -16,8 +16,7 @@ pub use serde_json;
|
||||
pub use wit::{
|
||||
download_file, make_file_executable,
|
||||
zed::extension::github::{
|
||||
github_release_by_tag_name, latest_github_release, GithubRelease, GithubReleaseAsset,
|
||||
GithubReleaseOptions,
|
||||
latest_github_release, GithubRelease, GithubReleaseAsset, GithubReleaseOptions,
|
||||
},
|
||||
zed::extension::nodejs::{
|
||||
node_binary_path, npm_install_package, npm_package_installed_version,
|
||||
@@ -141,7 +140,7 @@ pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "
|
||||
mod wit {
|
||||
wit_bindgen::generate!({
|
||||
skip: ["init-extension"],
|
||||
path: "./wit/since_v0.0.7",
|
||||
path: "./wit/since_v0.0.6",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[path = "../wit/since_v0.0.7/settings.rs"]
|
||||
#[path = "../wit/since_v0.0.6/settings.rs"]
|
||||
mod types;
|
||||
|
||||
use crate::{wit, Result, SettingsLocation, Worktree};
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
package zed:extension;
|
||||
|
||||
world extension {
|
||||
import github;
|
||||
import platform;
|
||||
import nodejs;
|
||||
|
||||
use lsp.{completion, symbol};
|
||||
|
||||
/// Initializes the extension.
|
||||
export init-extension: func();
|
||||
|
||||
/// The type of a downloaded file.
|
||||
enum downloaded-file-type {
|
||||
/// A gzipped file (`.gz`).
|
||||
gzip,
|
||||
/// A gzipped tar archive (`.tar.gz`).
|
||||
gzip-tar,
|
||||
/// A ZIP file (`.zip`).
|
||||
zip,
|
||||
/// An uncompressed file.
|
||||
uncompressed,
|
||||
}
|
||||
|
||||
/// The installation status for a language server.
|
||||
variant language-server-installation-status {
|
||||
/// The language server has no installation status.
|
||||
none,
|
||||
/// The language server is being downloaded.
|
||||
downloading,
|
||||
/// The language server is checking for updates.
|
||||
checking-for-update,
|
||||
/// The language server installation failed for specified reason.
|
||||
failed(string),
|
||||
}
|
||||
|
||||
record settings-location {
|
||||
worktree-id: u64,
|
||||
path: string,
|
||||
}
|
||||
|
||||
import get-settings: func(path: option<settings-location>, category: string, key: option<string>) -> result<string, string>;
|
||||
|
||||
/// Downloads a file from the given URL and saves it to the given path within the extension's
|
||||
/// working directory.
|
||||
///
|
||||
/// The file will be extracted according to the given file type.
|
||||
import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>;
|
||||
|
||||
/// Makes the file at the given path executable.
|
||||
import make-file-executable: func(filepath: string) -> result<_, string>;
|
||||
|
||||
/// Updates the installation status for the given language server.
|
||||
import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status);
|
||||
|
||||
/// A list of environment variables.
|
||||
type env-vars = list<tuple<string, string>>;
|
||||
|
||||
/// A command.
|
||||
record command {
|
||||
/// The command to execute.
|
||||
command: string,
|
||||
/// The arguments to pass to the command.
|
||||
args: list<string>,
|
||||
/// The environment variables to set for the command.
|
||||
env: env-vars,
|
||||
}
|
||||
|
||||
/// A Zed worktree.
|
||||
resource worktree {
|
||||
/// Returns the ID of the worktree.
|
||||
id: func() -> u64;
|
||||
/// Returns the root path of the worktree.
|
||||
root-path: func() -> string;
|
||||
/// Returns the textual contents of the specified file in the worktree.
|
||||
read-text-file: func(path: string) -> result<string, string>;
|
||||
/// Returns the path to the given binary name, if one is present on the `$PATH`.
|
||||
which: func(binary-name: string) -> option<string>;
|
||||
/// Returns the current shell environment.
|
||||
shell-env: func() -> env-vars;
|
||||
}
|
||||
|
||||
/// Returns the command used to start up the language server.
|
||||
export language-server-command: func(language-server-id: string, worktree: borrow<worktree>) -> result<command, string>;
|
||||
|
||||
/// Returns the initialization options to pass to the language server on startup.
|
||||
///
|
||||
/// The initialization options are represented as a JSON string.
|
||||
export language-server-initialization-options: func(language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
|
||||
|
||||
/// Returns the workspace configuration options to pass to the language server.
|
||||
export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
|
||||
|
||||
/// A label containing some code.
|
||||
record code-label {
|
||||
/// The source code to parse with Tree-sitter.
|
||||
code: string,
|
||||
/// The spans to display in the label.
|
||||
spans: list<code-label-span>,
|
||||
/// The range of the displayed label to include when filtering.
|
||||
filter-range: range,
|
||||
}
|
||||
|
||||
/// A span within a code label.
|
||||
variant code-label-span {
|
||||
/// A range into the parsed code.
|
||||
code-range(range),
|
||||
/// A span containing a code literal.
|
||||
literal(code-label-span-literal),
|
||||
}
|
||||
|
||||
/// A span containing a code literal.
|
||||
record code-label-span-literal {
|
||||
/// The literal text.
|
||||
text: string,
|
||||
/// The name of the highlight to use for this literal.
|
||||
highlight-name: option<string>,
|
||||
}
|
||||
|
||||
/// A (half-open) range (`[start, end)`).
|
||||
record range {
|
||||
/// The start of the range (inclusive).
|
||||
start: u32,
|
||||
/// The end of the range (exclusive).
|
||||
end: u32,
|
||||
}
|
||||
|
||||
export labels-for-completions: func(language-server-id: string, completions: list<completion>) -> result<list<option<code-label>>, string>;
|
||||
export labels-for-symbols: func(language-server-id: string, symbols: list<symbol>) -> result<list<option<code-label>>, string>;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
interface github {
|
||||
/// A GitHub release.
|
||||
record github-release {
|
||||
/// The version of the release.
|
||||
version: string,
|
||||
/// The list of assets attached to the release.
|
||||
assets: list<github-release-asset>,
|
||||
}
|
||||
|
||||
/// An asset from a GitHub release.
|
||||
record github-release-asset {
|
||||
/// The name of the asset.
|
||||
name: string,
|
||||
/// The download URL for the asset.
|
||||
download-url: string,
|
||||
}
|
||||
|
||||
/// The options used to filter down GitHub releases.
|
||||
record github-release-options {
|
||||
/// Whether releases without assets should be included.
|
||||
require-assets: bool,
|
||||
/// Whether pre-releases should be included.
|
||||
pre-release: bool,
|
||||
}
|
||||
|
||||
/// Returns the latest release for the given GitHub repository.
|
||||
latest-github-release: func(repo: string, options: github-release-options) -> result<github-release, string>;
|
||||
|
||||
/// Returns the GitHub release with the specified tag name for the given GitHub repository.
|
||||
///
|
||||
/// Returns an error if a release with the given tag name does not exist.
|
||||
github-release-by-tag-name: func(repo: string, tag: string) -> result<github-release, string>;
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
interface lsp {
|
||||
/// An LSP completion.
|
||||
record completion {
|
||||
label: string,
|
||||
detail: option<string>,
|
||||
kind: option<completion-kind>,
|
||||
insert-text-format: option<insert-text-format>,
|
||||
}
|
||||
|
||||
/// The kind of an LSP completion.
|
||||
variant completion-kind {
|
||||
text,
|
||||
method,
|
||||
function,
|
||||
%constructor,
|
||||
field,
|
||||
variable,
|
||||
class,
|
||||
%interface,
|
||||
module,
|
||||
property,
|
||||
unit,
|
||||
value,
|
||||
%enum,
|
||||
keyword,
|
||||
snippet,
|
||||
color,
|
||||
file,
|
||||
reference,
|
||||
folder,
|
||||
enum-member,
|
||||
constant,
|
||||
struct,
|
||||
event,
|
||||
operator,
|
||||
type-parameter,
|
||||
other(s32),
|
||||
}
|
||||
|
||||
/// Defines how to interpret the insert text in a completion item.
|
||||
variant insert-text-format {
|
||||
plain-text,
|
||||
snippet,
|
||||
other(s32),
|
||||
}
|
||||
|
||||
/// An LSP symbol.
|
||||
record symbol {
|
||||
kind: symbol-kind,
|
||||
name: string,
|
||||
}
|
||||
|
||||
/// The kind of an LSP symbol.
|
||||
variant symbol-kind {
|
||||
file,
|
||||
module,
|
||||
namespace,
|
||||
%package,
|
||||
class,
|
||||
method,
|
||||
property,
|
||||
field,
|
||||
%constructor,
|
||||
%enum,
|
||||
%interface,
|
||||
function,
|
||||
variable,
|
||||
constant,
|
||||
%string,
|
||||
number,
|
||||
boolean,
|
||||
array,
|
||||
object,
|
||||
key,
|
||||
null,
|
||||
enum-member,
|
||||
struct,
|
||||
event,
|
||||
operator,
|
||||
type-parameter,
|
||||
other(s32),
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
interface nodejs {
|
||||
/// Returns the path to the Node binary used by Zed.
|
||||
node-binary-path: func() -> result<string, string>;
|
||||
|
||||
/// Returns the latest version of the given NPM package.
|
||||
npm-package-latest-version: func(package-name: string) -> result<string, string>;
|
||||
|
||||
/// Returns the installed version of the given NPM package, if it exists.
|
||||
npm-package-installed-version: func(package-name: string) -> result<option<string>, string>;
|
||||
|
||||
/// Installs the specified NPM package.
|
||||
npm-install-package: func(package-name: string, version: string) -> result<_, string>;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
interface platform {
|
||||
/// An operating system.
|
||||
enum os {
|
||||
/// macOS.
|
||||
mac,
|
||||
/// Linux.
|
||||
linux,
|
||||
/// Windows.
|
||||
windows,
|
||||
}
|
||||
|
||||
/// A platform architecture.
|
||||
enum architecture {
|
||||
/// AArch64 (e.g., Apple Silicon).
|
||||
aarch64,
|
||||
/// x86.
|
||||
x86,
|
||||
/// x86-64.
|
||||
x8664,
|
||||
}
|
||||
|
||||
/// Gets the current operating system and architecture.
|
||||
current-platform: func() -> tuple<os, architecture>;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
/// The settings for a particular language.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LanguageSettings {
|
||||
/// How many columns a tab should occupy.
|
||||
pub tab_size: NonZeroU32,
|
||||
}
|
||||
|
||||
/// The settings for a particular language server.
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
pub struct LspSettings {
|
||||
/// The settings for the language server binary.
|
||||
pub binary: Option<BinarySettings>,
|
||||
/// The initialization options to pass to the language server.
|
||||
pub initialization_options: Option<serde_json::Value>,
|
||||
/// The settings to pass to language server.
|
||||
pub settings: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// The settings for a language server binary.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct BinarySettings {
|
||||
/// The path to the binary.
|
||||
pub path: Option<String>,
|
||||
/// The arguments to pass to the binary.
|
||||
pub arguments: Option<Vec<String>>,
|
||||
}
|
||||
@@ -26,7 +26,6 @@ gpui.workspace = true
|
||||
language.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
release_channel.workspace = true
|
||||
semantic_version.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
|
||||
@@ -9,7 +9,6 @@ use gpui::{
|
||||
prelude::*, AppContext, DismissEvent, EventEmitter, FocusableView, Task, View, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use release_channel::ReleaseChannel;
|
||||
use semantic_version::SemanticVersion;
|
||||
use settings::update_settings_file;
|
||||
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
|
||||
@@ -167,7 +166,7 @@ impl PickerDelegate for ExtensionVersionSelectorDelegate {
|
||||
let candidate_id = self.matches[self.selected_index].candidate_id;
|
||||
let extension_version = &self.extension_versions[candidate_id];
|
||||
|
||||
if !extension::is_version_compatible(ReleaseChannel::global(cx), extension_version) {
|
||||
if !extension::is_version_compatible(extension_version) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -197,13 +196,12 @@ impl PickerDelegate for ExtensionVersionSelectorDelegate {
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let version_match = &self.matches[ix];
|
||||
let extension_version = &self.extension_versions[version_match.candidate_id];
|
||||
|
||||
let is_version_compatible =
|
||||
extension::is_version_compatible(ReleaseChannel::global(cx), extension_version);
|
||||
let is_version_compatible = extension::is_version_compatible(extension_version);
|
||||
let disabled = !is_version_compatible;
|
||||
|
||||
Some(
|
||||
|
||||
@@ -16,7 +16,6 @@ use gpui::{
|
||||
FontWeight, InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,
|
||||
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
|
||||
};
|
||||
use release_channel::ReleaseChannel;
|
||||
use settings::Settings;
|
||||
use std::ops::DerefMut;
|
||||
use std::time::Duration;
|
||||
@@ -603,8 +602,7 @@ impl ExtensionsPage {
|
||||
has_dev_extension: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> (Button, Option<Button>) {
|
||||
let is_compatible =
|
||||
extension::is_version_compatible(ReleaseChannel::global(cx), &extension);
|
||||
let is_compatible = extension::is_version_compatible(&extension);
|
||||
|
||||
if has_dev_extension {
|
||||
// If we have a dev extension for the given extension, just treat it as uninstalled.
|
||||
|
||||
@@ -24,7 +24,6 @@ menu.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
settings.workspace = true
|
||||
serde.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
|
||||
@@ -3,13 +3,13 @@ mod file_finder_tests;
|
||||
|
||||
mod new_path_prompt;
|
||||
|
||||
use collections::{BTreeSet, HashMap};
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{scroll::Autoscroll, Bias, Editor};
|
||||
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
|
||||
use gpui::{
|
||||
actions, impl_actions, rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter,
|
||||
FocusHandle, FocusableView, Model, Modifiers, ModifiersChangedEvent, ParentElement, Render,
|
||||
Styled, Task, View, ViewContext, VisualContext, WeakView,
|
||||
actions, rems, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
|
||||
Model, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, View,
|
||||
ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use new_path_prompt::NewPathPrompt;
|
||||
@@ -29,14 +29,7 @@ use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
|
||||
use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
|
||||
use workspace::{item::PreviewTabsSettings, ModalView, Workspace};
|
||||
|
||||
actions!(file_finder, [SelectPrev]);
|
||||
impl_actions!(file_finder, [Toggle]);
|
||||
|
||||
#[derive(Default, PartialEq, Eq, Clone, serde::Deserialize)]
|
||||
pub struct Toggle {
|
||||
#[serde(default)]
|
||||
pub separate_history: bool,
|
||||
}
|
||||
actions!(file_finder, [Toggle, SelectPrev]);
|
||||
|
||||
impl ModalView for FileFinder {}
|
||||
|
||||
@@ -52,9 +45,9 @@ pub fn init(cx: &mut AppContext) {
|
||||
|
||||
impl FileFinder {
|
||||
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(|workspace, action: &Toggle, cx| {
|
||||
workspace.register_action(|workspace, _: &Toggle, cx| {
|
||||
let Some(file_finder) = workspace.active_modal::<Self>(cx) else {
|
||||
Self::open(workspace, action.separate_history, cx);
|
||||
Self::open(workspace, cx);
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -67,7 +60,7 @@ impl FileFinder {
|
||||
});
|
||||
}
|
||||
|
||||
fn open(workspace: &mut Workspace, separate_history: bool, cx: &mut ViewContext<Workspace>) {
|
||||
fn open(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
let project = workspace.project().read(cx);
|
||||
|
||||
let currently_opened_path = workspace
|
||||
@@ -99,7 +92,6 @@ impl FileFinder {
|
||||
project,
|
||||
currently_opened_path,
|
||||
history_items,
|
||||
separate_history,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -169,7 +161,6 @@ pub struct FileFinderDelegate {
|
||||
has_changed_selected_index: bool,
|
||||
cancel_flag: Arc<AtomicBool>,
|
||||
history_items: Vec<FoundPath>,
|
||||
separate_history: bool,
|
||||
}
|
||||
|
||||
/// Use a custom ordering for file finder: the regular one
|
||||
@@ -207,118 +198,104 @@ impl PartialOrd for ProjectPanelOrdMatch {
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct Matches {
|
||||
separate_history: bool,
|
||||
matches: Vec<Match>,
|
||||
history: Vec<(FoundPath, Option<ProjectPanelOrdMatch>)>,
|
||||
search: Vec<ProjectPanelOrdMatch>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
enum Match {
|
||||
History(FoundPath, Option<ProjectPanelOrdMatch>),
|
||||
Search(ProjectPanelOrdMatch),
|
||||
#[derive(Debug)]
|
||||
enum Match<'a> {
|
||||
History(&'a FoundPath, Option<&'a ProjectPanelOrdMatch>),
|
||||
Search(&'a ProjectPanelOrdMatch),
|
||||
}
|
||||
|
||||
impl Matches {
|
||||
fn len(&self) -> usize {
|
||||
self.matches.len()
|
||||
self.history.len() + self.search.len()
|
||||
}
|
||||
|
||||
fn get(&self, index: usize) -> Option<&Match> {
|
||||
self.matches.get(index)
|
||||
fn get(&self, index: usize) -> Option<Match<'_>> {
|
||||
if index < self.history.len() {
|
||||
self.history
|
||||
.get(index)
|
||||
.map(|(path, path_match)| Match::History(path, path_match.as_ref()))
|
||||
} else {
|
||||
self.search
|
||||
.get(index - self.history.len())
|
||||
.map(Match::Search)
|
||||
}
|
||||
}
|
||||
|
||||
fn push_new_matches<'a>(
|
||||
&'a mut self,
|
||||
history_items: impl IntoIterator<Item = &'a FoundPath> + Clone,
|
||||
currently_opened: Option<&'a FoundPath>,
|
||||
query: Option<&PathLikeWithPosition<FileSearchQuery>>,
|
||||
fn push_new_matches(
|
||||
&mut self,
|
||||
history_items: &Vec<FoundPath>,
|
||||
currently_opened: Option<&FoundPath>,
|
||||
query: &PathLikeWithPosition<FileSearchQuery>,
|
||||
new_search_matches: impl Iterator<Item = ProjectPanelOrdMatch>,
|
||||
extend_old_matches: bool,
|
||||
) {
|
||||
let no_history_score = 0;
|
||||
let matching_history_paths =
|
||||
matching_history_item_paths(history_items.clone(), currently_opened, query);
|
||||
matching_history_item_paths(history_items, currently_opened, query);
|
||||
let new_search_matches = new_search_matches
|
||||
.filter(|path_match| !matching_history_paths.contains_key(&path_match.0.path))
|
||||
.map(Match::Search)
|
||||
.map(|m| (no_history_score, m));
|
||||
let old_search_matches = self
|
||||
.matches
|
||||
.drain(..)
|
||||
.filter(|_| extend_old_matches)
|
||||
.filter(|m| matches!(m, Match::Search(_)))
|
||||
.map(|m| (no_history_score, m));
|
||||
let history_matches = history_items
|
||||
.filter(|path_match| !matching_history_paths.contains_key(&path_match.0.path));
|
||||
|
||||
self.set_new_history(
|
||||
currently_opened,
|
||||
Some(&matching_history_paths),
|
||||
history_items,
|
||||
);
|
||||
if extend_old_matches {
|
||||
self.search
|
||||
.retain(|path_match| !matching_history_paths.contains_key(&path_match.0.path));
|
||||
} else {
|
||||
self.search.clear();
|
||||
}
|
||||
util::extend_sorted(&mut self.search, new_search_matches, 100, |a, b| b.cmp(a));
|
||||
}
|
||||
|
||||
fn set_new_history<'a>(
|
||||
&mut self,
|
||||
currently_opened: Option<&'a FoundPath>,
|
||||
query_matches: Option<&'a HashMap<Arc<Path>, ProjectPanelOrdMatch>>,
|
||||
history_items: impl IntoIterator<Item = &'a FoundPath> + 'a,
|
||||
) {
|
||||
let mut processed_paths = HashSet::default();
|
||||
self.history = history_items
|
||||
.into_iter()
|
||||
.chain(currently_opened)
|
||||
.enumerate()
|
||||
.filter_map(|(i, history_item)| {
|
||||
let query_match = matching_history_paths
|
||||
.get(&history_item.project.path)
|
||||
.cloned();
|
||||
let query_match = if query.is_some() {
|
||||
query_match?
|
||||
} else {
|
||||
query_match.flatten()
|
||||
};
|
||||
Some((i + 1, Match::History(history_item.clone(), query_match)))
|
||||
});
|
||||
|
||||
let mut unique_matches = BTreeSet::new();
|
||||
self.matches = old_search_matches
|
||||
.chain(history_matches)
|
||||
.chain(new_search_matches)
|
||||
.filter(|(_, m)| unique_matches.insert(m.clone()))
|
||||
.sorted_by(|(history_score_a, a), (history_score_b, b)| {
|
||||
match (a, b) {
|
||||
// bubble currently opened files to the top
|
||||
(Match::History(path, _), _) if Some(path) == currently_opened => {
|
||||
cmp::Ordering::Less
|
||||
}
|
||||
(_, Match::History(path, _)) if Some(path) == currently_opened => {
|
||||
cmp::Ordering::Greater
|
||||
}
|
||||
|
||||
(Match::History(_, _), Match::Search(_)) if self.separate_history => {
|
||||
cmp::Ordering::Less
|
||||
}
|
||||
(Match::Search(_), Match::History(_, _)) if self.separate_history => {
|
||||
cmp::Ordering::Greater
|
||||
}
|
||||
|
||||
(Match::History(_, match_a), Match::History(_, match_b)) => {
|
||||
match_b.cmp(match_a)
|
||||
}
|
||||
(Match::History(_, match_a), Match::Search(match_b)) => {
|
||||
Some(match_b).cmp(&match_a.as_ref())
|
||||
}
|
||||
(Match::Search(match_a), Match::History(_, match_b)) => {
|
||||
match_b.as_ref().cmp(&Some(match_a))
|
||||
}
|
||||
(Match::Search(match_a), Match::Search(match_b)) => match_b.cmp(match_a),
|
||||
}
|
||||
.then(history_score_a.cmp(history_score_b))
|
||||
.filter(|&path| processed_paths.insert(path))
|
||||
.filter_map(|history_item| match &query_matches {
|
||||
Some(query_matches) => Some((
|
||||
history_item.clone(),
|
||||
Some(query_matches.get(&history_item.project.path)?.clone()),
|
||||
)),
|
||||
None => Some((history_item.clone(), None)),
|
||||
})
|
||||
.take(100)
|
||||
.map(|(_, m)| m)
|
||||
.enumerate()
|
||||
.sorted_by(
|
||||
|(index_a, (path_a, match_a)), (index_b, (path_b, match_b))| match (
|
||||
Some(path_a) == currently_opened,
|
||||
Some(path_b) == currently_opened,
|
||||
) {
|
||||
// bubble currently opened files to the top
|
||||
(true, false) => cmp::Ordering::Less,
|
||||
(false, true) => cmp::Ordering::Greater,
|
||||
// arrange the files by their score (best score on top) and by their occurrence in the history
|
||||
// (history items visited later are on the top)
|
||||
_ => match_b.cmp(match_a).then(index_a.cmp(index_b)),
|
||||
},
|
||||
)
|
||||
.map(|(_, paths)| paths)
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
||||
fn matching_history_item_paths<'a>(
|
||||
history_items: impl IntoIterator<Item = &'a FoundPath>,
|
||||
currently_opened: Option<&'a FoundPath>,
|
||||
query: Option<&PathLikeWithPosition<FileSearchQuery>>,
|
||||
) -> HashMap<Arc<Path>, Option<ProjectPanelOrdMatch>> {
|
||||
let Some(query) = query else {
|
||||
return history_items
|
||||
.into_iter()
|
||||
.chain(currently_opened)
|
||||
.map(|found_path| (Arc::clone(&found_path.project.path), None))
|
||||
.collect();
|
||||
};
|
||||
|
||||
fn matching_history_item_paths(
|
||||
history_items: &Vec<FoundPath>,
|
||||
currently_opened: Option<&FoundPath>,
|
||||
query: &PathLikeWithPosition<FileSearchQuery>,
|
||||
) -> HashMap<Arc<Path>, ProjectPanelOrdMatch> {
|
||||
let history_items_by_worktrees = history_items
|
||||
.into_iter()
|
||||
.iter()
|
||||
.chain(currently_opened)
|
||||
.filter_map(|found_path| {
|
||||
let candidate = PathMatchCandidate {
|
||||
@@ -363,7 +340,7 @@ fn matching_history_item_paths<'a>(
|
||||
.map(|path_match| {
|
||||
(
|
||||
Arc::clone(&path_match.path),
|
||||
Some(ProjectPanelOrdMatch(path_match)),
|
||||
ProjectPanelOrdMatch(path_match),
|
||||
)
|
||||
}),
|
||||
);
|
||||
@@ -422,7 +399,6 @@ impl FileFinderDelegate {
|
||||
project: Model<Project>,
|
||||
currently_opened_path: Option<FoundPath>,
|
||||
history_items: Vec<FoundPath>,
|
||||
separate_history: bool,
|
||||
cx: &mut ViewContext<FileFinder>,
|
||||
) -> Self {
|
||||
Self::subscribe_to_updates(&project, cx);
|
||||
@@ -440,7 +416,6 @@ impl FileFinderDelegate {
|
||||
selected_index: 0,
|
||||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||
history_items,
|
||||
separate_history,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,7 +510,7 @@ impl FileFinderDelegate {
|
||||
self.matches.push_new_matches(
|
||||
&self.history_items,
|
||||
self.currently_opened_path.as_ref(),
|
||||
Some(&query),
|
||||
&query,
|
||||
matches.into_iter(),
|
||||
extend_old_matches,
|
||||
);
|
||||
@@ -548,7 +523,7 @@ impl FileFinderDelegate {
|
||||
|
||||
fn labels_for_match(
|
||||
&self,
|
||||
path_match: &Match,
|
||||
path_match: Match,
|
||||
cx: &AppContext,
|
||||
ix: usize,
|
||||
) -> (String, Vec<usize>, String, Vec<usize>) {
|
||||
@@ -752,21 +727,12 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
}
|
||||
|
||||
fn separators_after_indices(&self) -> Vec<usize> {
|
||||
if self.separate_history {
|
||||
let first_non_history_index = self
|
||||
.matches
|
||||
.matches
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, m)| !matches!(m, Match::History(_, _)))
|
||||
.map(|(i, _)| i);
|
||||
if let Some(first_non_history_index) = first_non_history_index {
|
||||
if first_non_history_index > 0 {
|
||||
return vec![first_non_history_index - 1];
|
||||
}
|
||||
}
|
||||
let history_items = self.matches.history.len();
|
||||
if history_items == 0 || self.matches.search.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![history_items - 1]
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
@@ -780,20 +746,18 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
let project = self.project.read(cx);
|
||||
self.latest_search_id = post_inc(&mut self.search_count);
|
||||
self.matches = Matches {
|
||||
separate_history: self.separate_history,
|
||||
..Matches::default()
|
||||
history: Vec::new(),
|
||||
search: Vec::new(),
|
||||
};
|
||||
self.matches.push_new_matches(
|
||||
self.matches.set_new_history(
|
||||
self.currently_opened_path.as_ref(),
|
||||
None,
|
||||
self.history_items.iter().filter(|history_item| {
|
||||
project
|
||||
.worktree_for_id(history_item.project.worktree_id, cx)
|
||||
.is_some()
|
||||
|| (project.is_local() && history_item.absolute.is_some())
|
||||
}),
|
||||
self.currently_opened_path.as_ref(),
|
||||
None,
|
||||
None.into_iter(),
|
||||
false,
|
||||
);
|
||||
|
||||
self.selected_index = 0;
|
||||
@@ -955,23 +919,12 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
.get(ix)
|
||||
.expect("Invalid matches state: no element for index {ix}");
|
||||
|
||||
let icon = match &path_match {
|
||||
Match::History(_, _) => Icon::new(IconName::HistoryRerun)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
Match::Search(_) => v_flex()
|
||||
.flex_none()
|
||||
.size(IconSize::Small.rems())
|
||||
.into_any_element(),
|
||||
};
|
||||
let (file_name, file_name_positions, full_path, full_path_positions) =
|
||||
self.labels_for_match(path_match, cx, ix);
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.end_slot::<AnyElement>(Some(icon))
|
||||
.inset(true)
|
||||
.selected(selected)
|
||||
.child(
|
||||
|
||||
@@ -116,7 +116,7 @@ async fn test_absolute_paths(cx: &mut TestAppContext) {
|
||||
.await;
|
||||
picker.update(cx, |picker, _| {
|
||||
assert_eq!(
|
||||
collect_search_matches(picker).search_paths_only(),
|
||||
collect_search_matches(picker).search_only(),
|
||||
vec![PathBuf::from("a/b/file2.txt")],
|
||||
"Matching abs path should be the only match"
|
||||
)
|
||||
@@ -138,7 +138,7 @@ async fn test_absolute_paths(cx: &mut TestAppContext) {
|
||||
.await;
|
||||
picker.update(cx, |picker, _| {
|
||||
assert_eq!(
|
||||
collect_search_matches(picker).search_paths_only(),
|
||||
collect_search_matches(picker).search_only(),
|
||||
Vec::<PathBuf>::new(),
|
||||
"Mismatching abs path should produce no matches"
|
||||
)
|
||||
@@ -171,7 +171,7 @@ async fn test_complex_path(cx: &mut TestAppContext) {
|
||||
picker.update(cx, |picker, _| {
|
||||
assert_eq!(picker.delegate.matches.len(), 1);
|
||||
assert_eq!(
|
||||
collect_search_matches(picker).search_paths_only(),
|
||||
collect_search_matches(picker).search_only(),
|
||||
vec![PathBuf::from("其他/S数据表格/task.xlsx")],
|
||||
)
|
||||
});
|
||||
@@ -369,8 +369,12 @@ async fn test_matching_cancellation(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
picker.update(cx, |picker, cx| {
|
||||
let matches = collect_search_matches(picker).search_matches_only();
|
||||
let delegate = &mut picker.delegate;
|
||||
assert!(
|
||||
delegate.matches.history.is_empty(),
|
||||
"Search matches expected"
|
||||
);
|
||||
let matches = delegate.matches.search.clone();
|
||||
|
||||
// Simulate a search being cancelled after the time limit,
|
||||
// returning only a subset of the matches that would have been found.
|
||||
@@ -379,10 +383,7 @@ async fn test_matching_cancellation(cx: &mut TestAppContext) {
|
||||
delegate.latest_search_id,
|
||||
true, // did-cancel
|
||||
query.clone(),
|
||||
vec![
|
||||
ProjectPanelOrdMatch(matches[1].clone()),
|
||||
ProjectPanelOrdMatch(matches[3].clone()),
|
||||
],
|
||||
vec![matches[1].clone(), matches[3].clone()],
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -392,20 +393,15 @@ async fn test_matching_cancellation(cx: &mut TestAppContext) {
|
||||
delegate.latest_search_id,
|
||||
true, // did-cancel
|
||||
query.clone(),
|
||||
vec![
|
||||
ProjectPanelOrdMatch(matches[0].clone()),
|
||||
ProjectPanelOrdMatch(matches[2].clone()),
|
||||
ProjectPanelOrdMatch(matches[3].clone()),
|
||||
],
|
||||
vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
collect_search_matches(picker)
|
||||
.search_matches_only()
|
||||
.as_slice(),
|
||||
&matches[0..4]
|
||||
assert!(
|
||||
delegate.matches.history.is_empty(),
|
||||
"Search matches expected"
|
||||
);
|
||||
assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -484,11 +480,15 @@ async fn test_single_file_worktrees(cx: &mut TestAppContext) {
|
||||
cx.read(|cx| {
|
||||
let picker = picker.read(cx);
|
||||
let delegate = &picker.delegate;
|
||||
let matches = collect_search_matches(picker).search_matches_only();
|
||||
assert!(
|
||||
delegate.matches.history.is_empty(),
|
||||
"Search matches expected"
|
||||
);
|
||||
let matches = delegate.matches.search.clone();
|
||||
assert_eq!(matches.len(), 1);
|
||||
|
||||
let (file_name, file_name_positions, full_path, full_path_positions) =
|
||||
delegate.labels_for_path_match(&matches[0]);
|
||||
delegate.labels_for_path_match(&matches[0].0);
|
||||
assert_eq!(file_name, "the-file");
|
||||
assert_eq!(file_name_positions, &[0, 1, 4]);
|
||||
assert_eq!(full_path, "");
|
||||
@@ -552,10 +552,15 @@ async fn test_path_distance_ordering(cx: &mut TestAppContext) {
|
||||
})
|
||||
.await;
|
||||
|
||||
finder.update(cx, |picker, _| {
|
||||
let matches = collect_search_matches(picker).search_paths_only();
|
||||
assert_eq!(matches[0].as_path(), Path::new("dir2/a.txt"));
|
||||
assert_eq!(matches[1].as_path(), Path::new("dir1/a.txt"));
|
||||
finder.update(cx, |f, _| {
|
||||
let delegate = &f.delegate;
|
||||
assert!(
|
||||
delegate.matches.history.is_empty(),
|
||||
"Search matches expected"
|
||||
);
|
||||
let matches = &delegate.matches.search;
|
||||
assert_eq!(matches[0].0.path.as_ref(), Path::new("dir2/a.txt"));
|
||||
assert_eq!(matches[1].0.path.as_ref(), Path::new("dir1/a.txt"));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -872,7 +877,7 @@ async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
|
||||
let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
|
||||
|
||||
for expected_selected_index in 0..current_history.len() {
|
||||
cx.dispatch_action(Toggle::default());
|
||||
cx.dispatch_action(Toggle);
|
||||
let picker = active_file_picker(&workspace, cx);
|
||||
let selected_index = picker.update(cx, |picker, _| picker.delegate.selected_index());
|
||||
assert_eq!(
|
||||
@@ -881,7 +886,7 @@ async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
cx.dispatch_action(Toggle::default());
|
||||
cx.dispatch_action(Toggle);
|
||||
let selected_index = workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_modal::<FileFinder>(cx)
|
||||
@@ -940,19 +945,20 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
|
||||
finder.delegate.update_matches(first_query.to_string(), cx)
|
||||
})
|
||||
.await;
|
||||
finder.update(cx, |picker, _| {
|
||||
let matches = collect_search_matches(picker);
|
||||
assert_eq!(matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
|
||||
let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
|
||||
assert_eq!(history_match, &FoundPath::new(
|
||||
finder.update(cx, |finder, _| {
|
||||
let delegate = &finder.delegate;
|
||||
assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
|
||||
let history_match = delegate.matches.history.first().unwrap();
|
||||
assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
|
||||
assert_eq!(history_match.0, FoundPath::new(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/first.rs")),
|
||||
},
|
||||
Some(PathBuf::from("/src/test/first.rs"))
|
||||
));
|
||||
assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
|
||||
assert_eq!(matches.search.first().unwrap(), Path::new("test/fourth.rs"));
|
||||
assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
|
||||
assert_eq!(delegate.matches.search.first().unwrap().0.path.as_ref(), Path::new("test/fourth.rs"));
|
||||
});
|
||||
|
||||
let second_query = "fsdasdsa";
|
||||
@@ -962,11 +968,14 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
|
||||
finder.delegate.update_matches(second_query.to_string(), cx)
|
||||
})
|
||||
.await;
|
||||
finder.update(cx, |picker, _| {
|
||||
finder.update(cx, |finder, _| {
|
||||
let delegate = &finder.delegate;
|
||||
assert!(
|
||||
collect_search_matches(picker)
|
||||
.search_paths_only()
|
||||
.is_empty(),
|
||||
delegate.matches.history.is_empty(),
|
||||
"No history entries should match {second_query}"
|
||||
);
|
||||
assert!(
|
||||
delegate.matches.search.is_empty(),
|
||||
"No search entries should match {second_query}"
|
||||
);
|
||||
});
|
||||
@@ -981,19 +990,20 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
|
||||
.update_matches(first_query_again.to_string(), cx)
|
||||
})
|
||||
.await;
|
||||
finder.update(cx, |picker, _| {
|
||||
let matches = collect_search_matches(picker);
|
||||
assert_eq!(matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query");
|
||||
let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
|
||||
assert_eq!(history_match, &FoundPath::new(
|
||||
finder.update(cx, |finder, _| {
|
||||
let delegate = &finder.delegate;
|
||||
assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query");
|
||||
let history_match = delegate.matches.history.first().unwrap();
|
||||
assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
|
||||
assert_eq!(history_match.0, FoundPath::new(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/first.rs")),
|
||||
},
|
||||
Some(PathBuf::from("/src/test/first.rs"))
|
||||
));
|
||||
assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
|
||||
assert_eq!(matches.search.first().unwrap(), Path::new("test/fourth.rs"));
|
||||
assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
|
||||
assert_eq!(delegate.matches.search.first().unwrap().0.path.as_ref(), Path::new("test/fourth.rs"));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1129,9 +1139,6 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
|
||||
assert_eq!(finder.delegate.matches.len(), 5);
|
||||
assert_match_at_position(finder, 0, "main.rs");
|
||||
assert_match_selection(finder, 1, "bar.rs");
|
||||
assert_match_at_position(finder, 2, "lib.rs");
|
||||
assert_match_at_position(finder, 3, "moo.rs");
|
||||
assert_match_at_position(finder, 4, "maaa.rs");
|
||||
});
|
||||
|
||||
// main.rs is not among matches, select top item
|
||||
@@ -1143,7 +1150,6 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
|
||||
picker.update(cx, |finder, _| {
|
||||
assert_eq!(finder.delegate.matches.len(), 2);
|
||||
assert_match_at_position(finder, 0, "bar.rs");
|
||||
assert_match_at_position(finder, 1, "lib.rs");
|
||||
});
|
||||
|
||||
// main.rs is back, put it on top and select next item
|
||||
@@ -1156,7 +1162,6 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
|
||||
assert_eq!(finder.delegate.matches.len(), 3);
|
||||
assert_match_at_position(finder, 0, "main.rs");
|
||||
assert_match_selection(finder, 1, "moo.rs");
|
||||
assert_match_at_position(finder, 2, "maaa.rs");
|
||||
});
|
||||
|
||||
// get back to the initial state
|
||||
@@ -1169,99 +1174,6 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
|
||||
assert_eq!(finder.delegate.matches.len(), 3);
|
||||
assert_match_selection(finder, 0, "main.rs");
|
||||
assert_match_at_position(finder, 1, "lib.rs");
|
||||
assert_match_at_position(finder, 2, "bar.rs");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_non_separate_history_items(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/src",
|
||||
json!({
|
||||
"test": {
|
||||
"bar.rs": "// Bar file",
|
||||
"lib.rs": "// Lib file",
|
||||
"maaa.rs": "// Maaaaaaa",
|
||||
"main.rs": "// Main file",
|
||||
"moo.rs": "// Moooooo",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
||||
|
||||
open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
|
||||
open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
|
||||
open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
|
||||
|
||||
cx.dispatch_action(Toggle::default());
|
||||
let picker = active_file_picker(&workspace, cx);
|
||||
// main.rs is on top, previously used is selected
|
||||
picker.update(cx, |finder, _| {
|
||||
assert_eq!(finder.delegate.matches.len(), 3);
|
||||
assert_match_selection(finder, 0, "main.rs");
|
||||
assert_match_at_position(finder, 1, "lib.rs");
|
||||
assert_match_at_position(finder, 2, "bar.rs");
|
||||
});
|
||||
|
||||
// all files match, main.rs is still on top, but the second item is selected
|
||||
picker
|
||||
.update(cx, |finder, cx| {
|
||||
finder.delegate.update_matches(".rs".to_string(), cx)
|
||||
})
|
||||
.await;
|
||||
picker.update(cx, |finder, _| {
|
||||
assert_eq!(finder.delegate.matches.len(), 5);
|
||||
assert_match_at_position(finder, 0, "main.rs");
|
||||
assert_match_selection(finder, 1, "moo.rs");
|
||||
assert_match_at_position(finder, 2, "bar.rs");
|
||||
assert_match_at_position(finder, 3, "lib.rs");
|
||||
assert_match_at_position(finder, 4, "maaa.rs");
|
||||
});
|
||||
|
||||
// main.rs is not among matches, select top item
|
||||
picker
|
||||
.update(cx, |finder, cx| {
|
||||
finder.delegate.update_matches("b".to_string(), cx)
|
||||
})
|
||||
.await;
|
||||
picker.update(cx, |finder, _| {
|
||||
assert_eq!(finder.delegate.matches.len(), 2);
|
||||
assert_match_at_position(finder, 0, "bar.rs");
|
||||
assert_match_at_position(finder, 1, "lib.rs");
|
||||
});
|
||||
|
||||
// main.rs is back, put it on top and select next item
|
||||
picker
|
||||
.update(cx, |finder, cx| {
|
||||
finder.delegate.update_matches("m".to_string(), cx)
|
||||
})
|
||||
.await;
|
||||
picker.update(cx, |finder, _| {
|
||||
assert_eq!(finder.delegate.matches.len(), 3);
|
||||
assert_match_at_position(finder, 0, "main.rs");
|
||||
assert_match_selection(finder, 1, "moo.rs");
|
||||
assert_match_at_position(finder, 2, "maaa.rs");
|
||||
});
|
||||
|
||||
// get back to the initial state
|
||||
picker
|
||||
.update(cx, |finder, cx| {
|
||||
finder.delegate.update_matches("".to_string(), cx)
|
||||
})
|
||||
.await;
|
||||
picker.update(cx, |finder, _| {
|
||||
assert_eq!(finder.delegate.matches.len(), 3);
|
||||
assert_match_selection(finder, 0, "main.rs");
|
||||
assert_match_at_position(finder, 1, "lib.rs");
|
||||
assert_match_at_position(finder, 2, "bar.rs");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1354,8 +1266,19 @@ async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppCo
|
||||
let finder = open_file_picker(&workspace, cx);
|
||||
let query = "collab_ui";
|
||||
cx.simulate_input(query);
|
||||
finder.update(cx, |picker, _| {
|
||||
let search_entries = collect_search_matches(picker).search_paths_only();
|
||||
finder.update(cx, |finder, _| {
|
||||
let delegate = &finder.delegate;
|
||||
assert!(
|
||||
delegate.matches.history.is_empty(),
|
||||
"History items should not math query {query}, they should be matched by name only"
|
||||
);
|
||||
|
||||
let search_entries = delegate
|
||||
.matches
|
||||
.search
|
||||
.iter()
|
||||
.map(|path_match| path_match.0.path.to_path_buf())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
search_entries,
|
||||
vec![
|
||||
@@ -1398,9 +1321,15 @@ async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext)
|
||||
let picker = open_file_picker(&workspace, cx);
|
||||
cx.simulate_input("rs");
|
||||
|
||||
picker.update(cx, |picker, _| {
|
||||
picker.update(cx, |finder, _| {
|
||||
let history_entries = finder.delegate
|
||||
.matches
|
||||
.history
|
||||
.iter()
|
||||
.map(|(_, path_match)| path_match.as_ref().expect("should have a path match").0.path.to_path_buf())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
collect_search_matches(picker).history,
|
||||
history_entries,
|
||||
vec![
|
||||
PathBuf::from("test/first.rs"),
|
||||
PathBuf::from("test/third.rs"),
|
||||
@@ -1653,7 +1582,7 @@ async fn test_switches_between_release_norelease_modes_on_forward_nav(
|
||||
// Back to navigation with initial shortcut
|
||||
// Open file on modifiers release
|
||||
cx.simulate_modifiers_change(Modifiers::secondary_key());
|
||||
cx.dispatch_action(Toggle::default());
|
||||
cx.dispatch_action(Toggle);
|
||||
cx.simulate_modifiers_change(Modifiers::none());
|
||||
cx.read(|cx| {
|
||||
let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
|
||||
@@ -1847,9 +1776,7 @@ fn open_file_picker(
|
||||
workspace: &View<Workspace>,
|
||||
cx: &mut VisualTestContext,
|
||||
) -> View<Picker<FileFinderDelegate>> {
|
||||
cx.dispatch_action(Toggle {
|
||||
separate_history: true,
|
||||
});
|
||||
cx.dispatch_action(Toggle);
|
||||
active_file_picker(workspace, cx)
|
||||
}
|
||||
|
||||
@@ -1868,17 +1795,15 @@ fn active_file_picker(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug)]
|
||||
struct SearchEntries {
|
||||
history: Vec<PathBuf>,
|
||||
history_found_paths: Vec<FoundPath>,
|
||||
search: Vec<PathBuf>,
|
||||
search_matches: Vec<PathMatch>,
|
||||
}
|
||||
|
||||
impl SearchEntries {
|
||||
#[track_caller]
|
||||
fn search_paths_only(self) -> Vec<PathBuf> {
|
||||
fn search_only(self) -> Vec<PathBuf> {
|
||||
assert!(
|
||||
self.history.is_empty(),
|
||||
"Should have no history matches, but got: {:?}",
|
||||
@@ -1886,50 +1811,35 @@ impl SearchEntries {
|
||||
);
|
||||
self.search
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn search_matches_only(self) -> Vec<PathMatch> {
|
||||
assert!(
|
||||
self.history.is_empty(),
|
||||
"Should have no history matches, but got: {:?}",
|
||||
self.history
|
||||
);
|
||||
self.search_matches
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries {
|
||||
let mut search_entries = SearchEntries::default();
|
||||
for m in &picker.delegate.matches.matches {
|
||||
match m {
|
||||
Match::History(history_path, path_match) => {
|
||||
search_entries.history.push(
|
||||
path_match
|
||||
.as_ref()
|
||||
.map(|path_match| {
|
||||
Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
history_path
|
||||
.absolute
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| &history_path.project.path)
|
||||
.to_path_buf()
|
||||
}),
|
||||
);
|
||||
search_entries
|
||||
.history_found_paths
|
||||
.push(history_path.clone());
|
||||
}
|
||||
Match::Search(path_match) => {
|
||||
search_entries
|
||||
.search
|
||||
.push(Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path));
|
||||
search_entries.search_matches.push(path_match.0.clone());
|
||||
}
|
||||
}
|
||||
let matches = &picker.delegate.matches;
|
||||
SearchEntries {
|
||||
history: matches
|
||||
.history
|
||||
.iter()
|
||||
.map(|(history_path, path_match)| {
|
||||
path_match
|
||||
.as_ref()
|
||||
.map(|path_match| {
|
||||
Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
history_path
|
||||
.absolute
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| &history_path.project.path)
|
||||
.to_path_buf()
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
search: matches
|
||||
.search
|
||||
.iter()
|
||||
.map(|path_match| Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path))
|
||||
.collect(),
|
||||
}
|
||||
search_entries
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
|
||||
@@ -49,11 +49,6 @@ pub trait UpdateGlobal {
|
||||
where
|
||||
C: BorrowAppContext,
|
||||
F: FnOnce(&mut Self, &mut C) -> R;
|
||||
|
||||
/// Set the global instance of the implementing type.
|
||||
fn set_global<C>(cx: &mut C, global: Self)
|
||||
where
|
||||
C: BorrowAppContext;
|
||||
}
|
||||
|
||||
impl<T: Global> UpdateGlobal for T {
|
||||
@@ -64,11 +59,4 @@ impl<T: Global> UpdateGlobal for T {
|
||||
{
|
||||
cx.update_global(update)
|
||||
}
|
||||
|
||||
fn set_global<C>(cx: &mut C, global: Self)
|
||||
where
|
||||
C: BorrowAppContext,
|
||||
{
|
||||
cx.set_global(global)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,7 +599,6 @@ impl Platform for MacPlatform {
|
||||
panel.setCanChooseDirectories_(options.directories.to_objc());
|
||||
panel.setCanChooseFiles_(options.files.to_objc());
|
||||
panel.setAllowsMultipleSelection_(options.multiple.to_objc());
|
||||
panel.setCanCreateDirectories(true.to_objc());
|
||||
panel.setResolvesAliases_(false.to_objc());
|
||||
let done_tx = Cell::new(Some(done_tx));
|
||||
let block = ConcreteBlock::new(move |response: NSModalResponse| {
|
||||
|
||||
@@ -41,25 +41,6 @@ pub trait FluentBuilder {
|
||||
})
|
||||
}
|
||||
|
||||
/// Conditionally unwrap and modify self with one closure if the given option is Some, or another if it is None.
|
||||
fn when_some_else<T>(
|
||||
self,
|
||||
option: Option<T>,
|
||||
then: impl FnOnce(Self, T) -> Self,
|
||||
otherwise: impl FnOnce(Self) -> Self,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.map(|this| {
|
||||
if let Some(value) = option {
|
||||
then(this, value)
|
||||
} else {
|
||||
otherwise(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Conditionally modify self with one closure or another
|
||||
fn when_else(
|
||||
self,
|
||||
|
||||
@@ -280,14 +280,6 @@ impl<V: Render> From<View<V>> for AnyView {
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for AnyView {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.model == other.model
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for AnyView {}
|
||||
|
||||
impl Element for AnyView {
|
||||
type RequestLayoutState = Option<AnyElement>;
|
||||
type PrepaintState = Option<AnyElement>;
|
||||
|
||||
@@ -46,7 +46,6 @@ use std::{
|
||||
};
|
||||
use util::post_inc;
|
||||
use util::{measure, ResultExt};
|
||||
use uuid::Uuid;
|
||||
|
||||
mod prompts;
|
||||
|
||||
@@ -4444,9 +4443,6 @@ impl<V: 'static> From<WindowHandle<V>> for AnyWindowHandle {
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<V> Send for WindowHandle<V> {}
|
||||
unsafe impl<V> Sync for WindowHandle<V> {}
|
||||
|
||||
/// A handle to a window with any root view type, which can be downcast to a window with a specific root view type.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct AnyWindowHandle {
|
||||
@@ -4515,8 +4511,6 @@ pub enum ElementId {
|
||||
Integer(usize),
|
||||
/// A string based ID.
|
||||
Name(SharedString),
|
||||
/// A UUID.
|
||||
Uuid(Uuid),
|
||||
/// An ID that's equated with a focus handle.
|
||||
FocusHandle(FocusId),
|
||||
/// A combination of a name and an integer.
|
||||
@@ -4531,7 +4525,6 @@ impl Display for ElementId {
|
||||
ElementId::Name(name) => write!(f, "{}", name)?,
|
||||
ElementId::FocusHandle(_) => write!(f, "FocusHandle")?,
|
||||
ElementId::NamedInteger(s, i) => write!(f, "{}-{}", s, i)?,
|
||||
ElementId::Uuid(uuid) => write!(f, "{}", uuid)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -4598,18 +4591,6 @@ impl From<(&'static str, u64)> for ElementId {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Uuid> for ElementId {
|
||||
fn from(value: Uuid) -> Self {
|
||||
Self::Uuid(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&'static str, u32)> for ElementId {
|
||||
fn from((name, id): (&'static str, u32)) -> Self {
|
||||
ElementId::NamedInteger(name.into(), id as usize)
|
||||
}
|
||||
}
|
||||
|
||||
/// A rectangle to be rendered in the window at the given position and size.
|
||||
/// Passed as an argument [`WindowContext::paint_quad`].
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -76,47 +76,6 @@ pub async fn latest_github_release(
|
||||
.ok_or(anyhow!("Failed to find a release"))
|
||||
}
|
||||
|
||||
pub async fn get_release_by_tag_name(
|
||||
repo_name_with_owner: &str,
|
||||
tag: &str,
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> Result<GithubRelease, anyhow::Error> {
|
||||
let mut response = http
|
||||
.get(
|
||||
&format!("https://api.github.com/repos/{repo_name_with_owner}/releases/tags/{tag}"),
|
||||
Default::default(),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.context("error fetching latest release")?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("error reading latest release")?;
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let text = String::from_utf8_lossy(body.as_slice());
|
||||
bail!(
|
||||
"status error {}, response: {text:?}",
|
||||
response.status().as_u16()
|
||||
);
|
||||
}
|
||||
|
||||
let release = serde_json::from_slice::<GithubRelease>(body.as_slice()).map_err(|err| {
|
||||
log::error!("Error deserializing: {:?}", err);
|
||||
log::error!(
|
||||
"GitHub API response text: {:?}",
|
||||
String::from_utf8_lossy(body.as_slice())
|
||||
);
|
||||
anyhow!("error deserializing GitHub release")
|
||||
})?;
|
||||
|
||||
Ok(release)
|
||||
}
|
||||
|
||||
pub fn build_tarball_url(repo_name_with_owner: &str, tag: &str) -> Result<String> {
|
||||
let mut url = Url::parse(&format!(
|
||||
"https://github.com/{repo_name_with_owner}/archive/refs/tags",
|
||||
|
||||
@@ -3005,57 +3005,52 @@ impl BufferSnapshot {
|
||||
.map(|grammar| grammar.runnable_config.as_ref())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
iter::from_fn(move || loop {
|
||||
let mat = syntax_matches.peek()?;
|
||||
let test_range = test_configs[mat.grammar_index].and_then(|test_configs| {
|
||||
let mut tags: SmallVec<[(Range<usize>, RunnableTag); 1]> =
|
||||
SmallVec::from_iter(mat.captures.iter().filter_map(|capture| {
|
||||
test_configs
|
||||
.runnable_tags
|
||||
.get(&capture.index)
|
||||
.cloned()
|
||||
.map(|tag_name| (capture.node.byte_range(), tag_name))
|
||||
}));
|
||||
let maximum_range = tags
|
||||
.iter()
|
||||
.max_by_key(|(byte_range, _)| byte_range.len())
|
||||
.map(|(range, _)| range)?
|
||||
.clone();
|
||||
tags.sort_by_key(|(range, _)| range == &maximum_range);
|
||||
let split_point = tags.partition_point(|(range, _)| range != &maximum_range);
|
||||
let (extra_captures, tags) = tags.split_at(split_point);
|
||||
|
||||
let extra_captures = extra_captures
|
||||
.into_iter()
|
||||
.map(|(range, name)| {
|
||||
(
|
||||
name.0.to_string(),
|
||||
self.text_for_range(range.clone()).collect::<String>(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
Some(RunnableRange {
|
||||
run_range: mat
|
||||
.captures
|
||||
iter::from_fn(move || {
|
||||
let test_range = syntax_matches.peek().and_then(|mat| {
|
||||
test_configs[mat.grammar_index].and_then(|test_configs| {
|
||||
let mut tags: SmallVec<[(Range<usize>, RunnableTag); 1]> =
|
||||
SmallVec::from_iter(mat.captures.iter().filter_map(|capture| {
|
||||
test_configs
|
||||
.runnable_tags
|
||||
.get(&capture.index)
|
||||
.cloned()
|
||||
.map(|tag_name| (capture.node.byte_range(), tag_name))
|
||||
}));
|
||||
let maximum_range = tags
|
||||
.iter()
|
||||
.find(|capture| capture.index == test_configs.run_capture_ix)
|
||||
.map(|mat| mat.node.byte_range())?,
|
||||
runnable: Runnable {
|
||||
tags: tags.into_iter().cloned().map(|(_, tag)| tag).collect(),
|
||||
language: mat.language,
|
||||
buffer: self.remote_id(),
|
||||
},
|
||||
extra_captures,
|
||||
buffer_id: self.remote_id(),
|
||||
.max_by_key(|(byte_range, _)| byte_range.len())
|
||||
.map(|(range, _)| range)?
|
||||
.clone();
|
||||
tags.sort_by_key(|(range, _)| range == &maximum_range);
|
||||
let split_point = tags.partition_point(|(range, _)| range != &maximum_range);
|
||||
let (extra_captures, tags) = tags.split_at(split_point);
|
||||
let extra_captures = extra_captures
|
||||
.into_iter()
|
||||
.map(|(range, name)| {
|
||||
(
|
||||
name.0.to_string(),
|
||||
self.text_for_range(range.clone()).collect::<String>(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
Some(RunnableRange {
|
||||
run_range: mat
|
||||
.captures
|
||||
.iter()
|
||||
.find(|capture| capture.index == test_configs.run_capture_ix)
|
||||
.map(|mat| mat.node.byte_range())?,
|
||||
runnable: Runnable {
|
||||
tags: tags.into_iter().cloned().map(|(_, tag)| tag).collect(),
|
||||
language: mat.language,
|
||||
buffer: self.remote_id(),
|
||||
},
|
||||
extra_captures,
|
||||
buffer_id: self.remote_id(),
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
syntax_matches.advance();
|
||||
if test_range.is_some() {
|
||||
// It's fine for us to short-circuit on .peek()? returning None. We don't want to return None from this iter if we
|
||||
// had a capture that did not contain a run marker, hence we'll just loop around for the next capture.
|
||||
return test_range;
|
||||
}
|
||||
test_range
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,9 @@ use std::{
|
||||
};
|
||||
use syntax_map::{QueryCursorHandle, SyntaxSnapshot};
|
||||
use task::RunnableTag;
|
||||
pub use task_context::{ContextProvider, RunnableRange};
|
||||
pub use task_context::{
|
||||
BasicContextProvider, ContextProvider, ContextProviderWithTasks, RunnableRange,
|
||||
};
|
||||
use theme::SyntaxTheme;
|
||||
use tree_sitter::{self, wasmtime, Query, QueryCursor, WasmStore};
|
||||
|
||||
@@ -1014,7 +1016,7 @@ impl Language {
|
||||
for (ix, name) in query.capture_names().iter().enumerate() {
|
||||
if *name == "run" {
|
||||
run_capture_index = Some(ix as u32);
|
||||
} else {
|
||||
} else if !name.starts_with('_') {
|
||||
runnable_tags.insert(ix as u32, RunnableTag(name.to_string().into()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use std::ops::Range;
|
||||
use std::{ops::Range, path::Path};
|
||||
|
||||
use crate::{Location, Runnable};
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use gpui::AppContext;
|
||||
use task::{TaskTemplates, TaskVariables};
|
||||
use text::BufferId;
|
||||
use task::{TaskTemplates, TaskVariables, VariableName};
|
||||
use text::{BufferId, Point, ToPoint};
|
||||
|
||||
pub struct RunnableRange {
|
||||
pub buffer_id: BufferId,
|
||||
@@ -22,7 +22,7 @@ pub trait ContextProvider: Send + Sync {
|
||||
/// Builds a specific context to be placed on top of the basic one (replacing all conflicting entries) and to be used for task resolving later.
|
||||
fn build_context(
|
||||
&self,
|
||||
_variables: &TaskVariables,
|
||||
_worktree_abs_path: Option<&Path>,
|
||||
_location: &Location,
|
||||
_cx: &mut AppContext,
|
||||
) -> Result<TaskVariables> {
|
||||
@@ -33,4 +33,100 @@ pub trait ContextProvider: Send + Sync {
|
||||
fn associated_tasks(&self) -> Option<TaskTemplates> {
|
||||
None
|
||||
}
|
||||
|
||||
// Determines whether the [`BasicContextProvider`] variables should be filled too (if `false`), or omitted (if `true`).
|
||||
fn is_basic(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file.
|
||||
/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
|
||||
pub struct BasicContextProvider;
|
||||
|
||||
impl ContextProvider for BasicContextProvider {
|
||||
fn is_basic(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn build_context(
|
||||
&self,
|
||||
worktree_abs_path: Option<&Path>,
|
||||
location: &Location,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<TaskVariables> {
|
||||
let buffer = location.buffer.read(cx);
|
||||
let buffer_snapshot = buffer.snapshot();
|
||||
let symbols = buffer_snapshot.symbols_containing(location.range.start, None);
|
||||
let symbol = symbols.unwrap_or_default().last().map(|symbol| {
|
||||
let range = symbol
|
||||
.name_ranges
|
||||
.last()
|
||||
.cloned()
|
||||
.unwrap_or(0..symbol.text.len());
|
||||
symbol.text[range].to_string()
|
||||
});
|
||||
|
||||
let current_file = buffer
|
||||
.file()
|
||||
.and_then(|file| file.as_local())
|
||||
.map(|file| file.abs_path(cx).to_string_lossy().to_string());
|
||||
let Point { row, column } = location.range.start.to_point(&buffer_snapshot);
|
||||
let row = row + 1;
|
||||
let column = column + 1;
|
||||
let selected_text = buffer
|
||||
.chars_for_range(location.range.clone())
|
||||
.collect::<String>();
|
||||
|
||||
let mut task_variables = TaskVariables::from_iter([
|
||||
(VariableName::Row, row.to_string()),
|
||||
(VariableName::Column, column.to_string()),
|
||||
]);
|
||||
|
||||
if let Some(symbol) = symbol {
|
||||
task_variables.insert(VariableName::Symbol, symbol);
|
||||
}
|
||||
if !selected_text.trim().is_empty() {
|
||||
task_variables.insert(VariableName::SelectedText, selected_text);
|
||||
}
|
||||
if let Some(path) = current_file {
|
||||
task_variables.insert(VariableName::File, path);
|
||||
}
|
||||
if let Some(worktree_path) = worktree_abs_path {
|
||||
task_variables.insert(
|
||||
VariableName::WorktreeRoot,
|
||||
worktree_path.to_string_lossy().to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(task_variables)
|
||||
}
|
||||
}
|
||||
|
||||
/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks.
|
||||
pub struct ContextProviderWithTasks {
|
||||
templates: TaskTemplates,
|
||||
}
|
||||
|
||||
impl ContextProviderWithTasks {
|
||||
pub fn new(definitions: TaskTemplates) -> Self {
|
||||
Self {
|
||||
templates: definitions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextProvider for ContextProviderWithTasks {
|
||||
fn associated_tasks(&self) -> Option<TaskTemplates> {
|
||||
Some(self.templates.clone())
|
||||
}
|
||||
|
||||
fn build_context(
|
||||
&self,
|
||||
worktree_abs_path: Option<&Path>,
|
||||
location: &Location,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<TaskVariables> {
|
||||
BasicContextProvider.build_context(worktree_abs_path, location, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use project::ContextProviderWithTasks;
|
||||
use language::ContextProviderWithTasks;
|
||||
use task::{TaskTemplate, TaskTemplates, VariableName};
|
||||
|
||||
pub(super) fn bash_task_context() -> ContextProviderWithTasks {
|
||||
|
||||
@@ -13,7 +13,6 @@ use settings::Settings;
|
||||
use smol::{fs, process};
|
||||
use std::{
|
||||
any::Any,
|
||||
borrow::Cow,
|
||||
ffi::{OsStr, OsString},
|
||||
ops::Range,
|
||||
path::PathBuf,
|
||||
@@ -23,7 +22,6 @@ use std::{
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
|
||||
use util::{fs::remove_matching, maybe, ResultExt};
|
||||
|
||||
fn server_binary_arguments() -> Vec<OsString> {
|
||||
@@ -440,94 +438,6 @@ fn adjust_runs(
|
||||
runs
|
||||
}
|
||||
|
||||
pub(crate) struct GoContextProvider;
|
||||
|
||||
const GO_PACKAGE_TASK_VARIABLE: VariableName = VariableName::Custom(Cow::Borrowed("GO_PACKAGE"));
|
||||
|
||||
impl ContextProvider for GoContextProvider {
|
||||
fn build_context(
|
||||
&self,
|
||||
variables: &TaskVariables,
|
||||
location: &Location,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Result<TaskVariables> {
|
||||
let local_abs_path = location
|
||||
.buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.and_then(|file| Some(file.as_local()?.abs_path(cx)));
|
||||
|
||||
Ok(
|
||||
if let Some(buffer_dir) = local_abs_path
|
||||
.as_deref()
|
||||
.and_then(|local_abs_path| local_abs_path.parent())
|
||||
{
|
||||
// Prefer the relative form `./my-nested-package/is-here` over
|
||||
// absolute path, because it's more readable in the modal, but
|
||||
// the absolute path also works.
|
||||
let package_name = variables
|
||||
.get(&VariableName::WorktreeRoot)
|
||||
.and_then(|worktree_abs_path| buffer_dir.strip_prefix(worktree_abs_path).ok())
|
||||
.map(|relative_pkg_dir| {
|
||||
if relative_pkg_dir.as_os_str().is_empty() {
|
||||
".".into()
|
||||
} else {
|
||||
format!("./{}", relative_pkg_dir.to_string_lossy())
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| format!("{}", buffer_dir.to_string_lossy()));
|
||||
|
||||
TaskVariables::from_iter(Some((
|
||||
GO_PACKAGE_TASK_VARIABLE.clone(),
|
||||
package_name.to_string(),
|
||||
)))
|
||||
} else {
|
||||
TaskVariables::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn associated_tasks(&self) -> Option<TaskTemplates> {
|
||||
Some(TaskTemplates(vec![
|
||||
TaskTemplate {
|
||||
label: format!(
|
||||
"go test {} -run {}",
|
||||
GO_PACKAGE_TASK_VARIABLE.template_value(),
|
||||
VariableName::Symbol.template_value(),
|
||||
),
|
||||
command: "go".into(),
|
||||
args: vec![
|
||||
"test".into(),
|
||||
GO_PACKAGE_TASK_VARIABLE.template_value(),
|
||||
"-run".into(),
|
||||
VariableName::Symbol.template_value(),
|
||||
],
|
||||
tags: vec!["go-test".to_owned()],
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: format!("go test {}", GO_PACKAGE_TASK_VARIABLE.template_value()),
|
||||
command: "go".into(),
|
||||
args: vec!["test".into(), GO_PACKAGE_TASK_VARIABLE.template_value()],
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: "go test ./...".into(),
|
||||
command: "go".into(),
|
||||
args: vec!["test".into(), "./...".into()],
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: format!("go run {}", GO_PACKAGE_TASK_VARIABLE.template_value(),),
|
||||
command: "go".into(),
|
||||
args: vec!["run".into(), GO_PACKAGE_TASK_VARIABLE.template_value()],
|
||||
tags: vec!["go-main".to_owned()],
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -110,9 +110,6 @@
|
||||
(imaginary_literal)
|
||||
] @number
|
||||
|
||||
(const_spec
|
||||
name: (identifier) @constant)
|
||||
|
||||
[
|
||||
(true)
|
||||
(false)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
(
|
||||
(function_declaration name: (_) @run
|
||||
(#match? @run "^Test.*"))
|
||||
) @go-test
|
||||
|
||||
(
|
||||
(function_declaration name: (_) @run
|
||||
(#eq? @run "main"))
|
||||
) @go-main
|
||||
@@ -8,10 +8,7 @@ use smol::stream::StreamExt;
|
||||
use std::{str, sync::Arc};
|
||||
use util::{asset_str, ResultExt};
|
||||
|
||||
use crate::{
|
||||
bash::bash_task_context, go::GoContextProvider, python::python_task_context,
|
||||
rust::RustContextProvider,
|
||||
};
|
||||
use crate::{bash::bash_task_context, python::python_task_context, rust::RustContextProvider};
|
||||
|
||||
mod bash;
|
||||
mod c;
|
||||
@@ -106,13 +103,9 @@ pub fn init(
|
||||
"css",
|
||||
vec![Arc::new(css::CssLspAdapter::new(node_runtime.clone())),]
|
||||
);
|
||||
language!("go", vec![Arc::new(go::GoLspAdapter)], GoContextProvider);
|
||||
language!("gomod", vec![Arc::new(go::GoLspAdapter)], GoContextProvider);
|
||||
language!(
|
||||
"gowork",
|
||||
vec![Arc::new(go::GoLspAdapter)],
|
||||
GoContextProvider
|
||||
);
|
||||
language!("go", vec![Arc::new(go::GoLspAdapter)]);
|
||||
language!("gomod");
|
||||
language!("gowork");
|
||||
language!(
|
||||
"json",
|
||||
vec![Arc::new(json::JsonLspAdapter::new(
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use language::{ContextProviderWithTasks, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::ContextProviderWithTasks;
|
||||
use std::{
|
||||
any::Any,
|
||||
ffi::OsString,
|
||||
|
||||
@@ -328,7 +328,7 @@ const RUST_PACKAGE_TASK_VARIABLE: VariableName =
|
||||
impl ContextProvider for RustContextProvider {
|
||||
fn build_context(
|
||||
&self,
|
||||
_: &TaskVariables,
|
||||
_: Option<&Path>,
|
||||
location: &Location,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Result<TaskVariables> {
|
||||
|
||||
@@ -79,7 +79,6 @@ pub struct LanguageServer {
|
||||
io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
|
||||
output_done_rx: Mutex<Option<barrier::Receiver>>,
|
||||
root_path: PathBuf,
|
||||
working_dir: PathBuf,
|
||||
server: Arc<Mutex<Option<Child>>>,
|
||||
}
|
||||
|
||||
@@ -289,7 +288,6 @@ impl LanguageServer {
|
||||
stderr_capture,
|
||||
Some(server),
|
||||
root_path,
|
||||
working_dir,
|
||||
code_action_kinds,
|
||||
cx,
|
||||
move |notification| {
|
||||
@@ -324,7 +322,6 @@ impl LanguageServer {
|
||||
stderr_capture: Arc<Mutex<Option<String>>>,
|
||||
server: Option<Child>,
|
||||
root_path: &Path,
|
||||
working_dir: &Path,
|
||||
code_action_kinds: Option<Vec<CodeActionKind>>,
|
||||
cx: AsyncAppContext,
|
||||
on_unhandled_notification: F,
|
||||
@@ -396,7 +393,6 @@ impl LanguageServer {
|
||||
io_tasks: Mutex::new(Some((input_task, output_task))),
|
||||
output_done_rx: Mutex::new(Some(output_done_rx)),
|
||||
root_path: root_path.to_path_buf(),
|
||||
working_dir: working_dir.to_path_buf(),
|
||||
server: Arc::new(Mutex::new(server)),
|
||||
}
|
||||
}
|
||||
@@ -570,7 +566,7 @@ impl LanguageServer {
|
||||
options: Option<Value>,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<Arc<Self>>> {
|
||||
let root_uri = Url::from_file_path(&self.working_dir).unwrap();
|
||||
let root_uri = Url::from_file_path(&self.root_path).unwrap();
|
||||
#[allow(deprecated)]
|
||||
let params = InitializeParams {
|
||||
process_id: None,
|
||||
@@ -1175,7 +1171,6 @@ impl FakeLanguageServer {
|
||||
Arc::new(Mutex::new(None)),
|
||||
None,
|
||||
Path::new("/"),
|
||||
Path::new("/"),
|
||||
None,
|
||||
cx.clone(),
|
||||
|_| {},
|
||||
@@ -1192,7 +1187,6 @@ impl FakeLanguageServer {
|
||||
Arc::new(Mutex::new(None)),
|
||||
None,
|
||||
Path::new("/"),
|
||||
Path::new("/"),
|
||||
None,
|
||||
cx,
|
||||
move |msg| {
|
||||
|
||||
@@ -73,9 +73,6 @@ impl Markdown {
|
||||
}
|
||||
|
||||
pub fn reset(&mut self, source: String, cx: &mut ViewContext<Self>) {
|
||||
if source == self.source() {
|
||||
return;
|
||||
}
|
||||
self.source = source;
|
||||
self.selection = Selection::default();
|
||||
self.autoscroll_request = None;
|
||||
@@ -547,10 +544,8 @@ impl Element for MarkdownElement {
|
||||
})
|
||||
}
|
||||
MarkdownTag::Link { dest_url, .. } => {
|
||||
if builder.code_block_stack.is_empty() {
|
||||
builder.push_link(dest_url.clone(), range.clone());
|
||||
builder.push_text_style(self.style.link.clone())
|
||||
}
|
||||
builder.push_link(dest_url.clone(), range.clone());
|
||||
builder.push_text_style(self.style.link.clone())
|
||||
}
|
||||
_ => log::error!("unsupported markdown tag {:?}", tag),
|
||||
}
|
||||
@@ -582,11 +577,7 @@ impl Element for MarkdownElement {
|
||||
MarkdownTagEnd::Emphasis => builder.pop_text_style(),
|
||||
MarkdownTagEnd::Strong => builder.pop_text_style(),
|
||||
MarkdownTagEnd::Strikethrough => builder.pop_text_style(),
|
||||
MarkdownTagEnd::Link => {
|
||||
if builder.code_block_stack.is_empty() {
|
||||
builder.pop_text_style()
|
||||
}
|
||||
}
|
||||
MarkdownTagEnd::Link => builder.pop_text_style(),
|
||||
_ => log::error!("unsupported markdown tag end: {:?}", tag),
|
||||
},
|
||||
MarkdownEvent::Text => {
|
||||
|
||||
@@ -110,7 +110,6 @@ impl MultiBufferRow {
|
||||
pub const MIN: Self = Self(0);
|
||||
pub const MAX: Self = Self(u32::MAX);
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct History {
|
||||
next_transaction_id: TransactionId,
|
||||
@@ -1532,6 +1531,46 @@ impl MultiBuffer {
|
||||
.map(|state| state.buffer.clone())
|
||||
}
|
||||
|
||||
pub fn is_completion_trigger(
|
||||
&self,
|
||||
position: Anchor,
|
||||
text: &str,
|
||||
trigger_in_words: bool,
|
||||
cx: &AppContext,
|
||||
) -> bool {
|
||||
let mut chars = text.chars();
|
||||
let char = if let Some(char) = chars.next() {
|
||||
char
|
||||
} else {
|
||||
return false;
|
||||
};
|
||||
if chars.next().is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let snapshot = self.snapshot(cx);
|
||||
let position = position.to_offset(&snapshot);
|
||||
let scope = snapshot.language_scope_at(position);
|
||||
if trigger_in_words && char_kind(&scope, char) == CharKind::Word {
|
||||
return true;
|
||||
}
|
||||
|
||||
let anchor = snapshot.anchor_before(position);
|
||||
anchor
|
||||
.buffer_id
|
||||
.and_then(|buffer_id| {
|
||||
let buffer = self.buffers.borrow().get(&buffer_id)?.buffer.clone();
|
||||
Some(
|
||||
buffer
|
||||
.read(cx)
|
||||
.completion_triggers()
|
||||
.iter()
|
||||
.any(|string| string == text),
|
||||
)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn language_at<T: ToOffset>(&self, point: T, cx: &AppContext) -> Option<Arc<Language>> {
|
||||
self.point_to_buffer_offset(point, cx)
|
||||
.and_then(|(buffer, offset, _)| buffer.read(cx).language_at(offset))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use editor::{scroll::Autoscroll, CompletionProvider, Editor};
|
||||
use editor::{scroll::Autoscroll, Editor};
|
||||
use gpui::{
|
||||
actions, div, impl_actions, list, prelude::*, uniform_list, AnyElement, AppContext, ClickEvent,
|
||||
DismissEvent, EventEmitter, FocusHandle, FocusableView, Length, ListState, MouseButton,
|
||||
@@ -166,17 +166,6 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
Self::new(delegate, ContainerKind::List, head, cx)
|
||||
}
|
||||
|
||||
/// Adds a completion provider for this pickers query editor, if it has one.
|
||||
pub fn with_completions_provider(
|
||||
self,
|
||||
provider: Box<dyn CompletionProvider>,
|
||||
cx: &mut WindowContext<'_>,
|
||||
) -> Self {
|
||||
if let Head::Editor(editor) = &self.head {
|
||||
editor.update(cx, |this, _| this.set_completion_provider(provider))
|
||||
}
|
||||
self
|
||||
}
|
||||
fn new(delegate: D, container: ContainerKind, head: Head, cx: &mut ViewContext<Self>) -> Self {
|
||||
let mut this = Self {
|
||||
delegate,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::Context;
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::Fs;
|
||||
use gpui::{AsyncAppContext, Model};
|
||||
@@ -315,12 +315,6 @@ impl Prettier {
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if prettier_settings.parser.is_none() && buffer_path.is_none() {
|
||||
log::error!("Formatting unsaved file with prettier failed. No prettier parser configured for language");
|
||||
return Err(anyhow!("Cannot determine prettier parser for unsaved file"));
|
||||
}
|
||||
|
||||
log::debug!(
|
||||
"Formatting file {:?} with prettier, plugins :{:?}, options: {:?}",
|
||||
buffer.file().map(|f| f.full_path(cx)),
|
||||
@@ -339,7 +333,6 @@ impl Prettier {
|
||||
})
|
||||
})?
|
||||
.context("prettier params calculation")?;
|
||||
|
||||
let response = local
|
||||
.server
|
||||
.request::<Format>(params)
|
||||
|
||||
@@ -186,17 +186,11 @@ async function handleMessage(message, prettier) {
|
||||
}
|
||||
|
||||
let resolvedConfig = {};
|
||||
if (params.options.filepath) {
|
||||
if (params.options.filepath !== undefined) {
|
||||
resolvedConfig =
|
||||
(await prettier.prettier.resolveConfig(params.options.filepath)) || {};
|
||||
}
|
||||
|
||||
// Marking the params.options.filepath as undefined makes
|
||||
// prettier.format() work even if no filepath is set.
|
||||
if (params.options.filepath === null) {
|
||||
params.options.filepath = undefined;
|
||||
}
|
||||
|
||||
const plugins =
|
||||
Array.isArray(resolvedConfig?.plugins) &&
|
||||
resolvedConfig.plugins.length > 0
|
||||
|
||||
@@ -52,12 +52,10 @@ regex.workspace = true
|
||||
rpc.workspace = true
|
||||
schemars.workspace = true
|
||||
task.workspace = true
|
||||
tempfile.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
sha2.workspace = true
|
||||
shlex.workspace = true
|
||||
similar = "1.3"
|
||||
smol.workspace = true
|
||||
snippet.workspace = true
|
||||
|
||||
@@ -61,8 +61,7 @@ pub(super) async fn format_with_prettier(
|
||||
.update(cx, |buffer, cx| {
|
||||
File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
.ok()?;
|
||||
|
||||
let format_result = prettier
|
||||
.format(buffer, buffer_path, cx)
|
||||
|
||||
@@ -55,9 +55,9 @@ use language::{
|
||||
use log::error;
|
||||
use lsp::{
|
||||
DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions,
|
||||
DocumentHighlightKind, Edit, FileSystemWatcher, LanguageServer, LanguageServerBinary,
|
||||
LanguageServerId, LspRequestFuture, MessageActionItem, OneOf, ServerCapabilities,
|
||||
ServerHealthStatus, ServerStatus, TextEdit,
|
||||
DocumentHighlightKind, Edit, LanguageServer, LanguageServerBinary, LanguageServerId,
|
||||
LspRequestFuture, MessageActionItem, OneOf, ServerCapabilities, ServerHealthStatus,
|
||||
ServerStatus, TextEdit,
|
||||
};
|
||||
use lsp_command::*;
|
||||
use node_runtime::NodeRuntime;
|
||||
@@ -113,9 +113,7 @@ pub use fs::*;
|
||||
pub use language::Location;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX;
|
||||
pub use task_inventory::{
|
||||
BasicContextProvider, ContextProviderWithTasks, Inventory, TaskSourceKind,
|
||||
};
|
||||
pub use task_inventory::{Inventory, TaskSourceKind};
|
||||
pub use worktree::{
|
||||
DiagnosticSummary, Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId,
|
||||
RepositoryEntry, UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId,
|
||||
@@ -169,8 +167,6 @@ pub struct Project {
|
||||
last_formatting_failure: Option<String>,
|
||||
last_workspace_edits_by_language_server: HashMap<LanguageServerId, ProjectTransaction>,
|
||||
language_server_watched_paths: HashMap<LanguageServerId, HashMap<WorktreeId, GlobSet>>,
|
||||
language_server_watcher_registrations:
|
||||
HashMap<LanguageServerId, HashMap<String, Vec<FileSystemWatcher>>>,
|
||||
client: Arc<client::Client>,
|
||||
next_entry_id: Arc<AtomicUsize>,
|
||||
join_project_response_message_id: u32,
|
||||
@@ -729,7 +725,6 @@ impl Project {
|
||||
last_formatting_failure: None,
|
||||
last_workspace_edits_by_language_server: Default::default(),
|
||||
language_server_watched_paths: HashMap::default(),
|
||||
language_server_watcher_registrations: HashMap::default(),
|
||||
buffers_being_formatted: Default::default(),
|
||||
buffers_needing_diff: Default::default(),
|
||||
git_diff_debouncer: DebouncedDelay::new(),
|
||||
@@ -880,7 +875,6 @@ impl Project {
|
||||
last_formatting_failure: None,
|
||||
last_workspace_edits_by_language_server: Default::default(),
|
||||
language_server_watched_paths: HashMap::default(),
|
||||
language_server_watcher_registrations: HashMap::default(),
|
||||
opened_buffers: Default::default(),
|
||||
buffers_being_formatted: Default::default(),
|
||||
buffers_needing_diff: Default::default(),
|
||||
@@ -3477,7 +3471,7 @@ impl Project {
|
||||
let options = serde_json::from_value(options)?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.on_lsp_did_change_watched_files(
|
||||
server_id, ®.id, options, cx,
|
||||
server_id, options, cx,
|
||||
);
|
||||
})?;
|
||||
}
|
||||
@@ -3489,27 +3483,6 @@ impl Project {
|
||||
})
|
||||
.detach();
|
||||
|
||||
language_server
|
||||
.on_request::<lsp::request::UnregisterCapability, _, _>({
|
||||
let this = this.clone();
|
||||
move |params, mut cx| {
|
||||
let this = this.clone();
|
||||
async move {
|
||||
for unreg in params.unregisterations.iter() {
|
||||
if unreg.method == "workspace/didChangeWatchedFiles" {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.on_lsp_unregister_did_change_watched_files(
|
||||
server_id, &unreg.id, cx,
|
||||
);
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
language_server
|
||||
.on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
|
||||
let adapter = adapter.clone();
|
||||
@@ -4235,67 +4208,16 @@ impl Project {
|
||||
fn on_lsp_did_change_watched_files(
|
||||
&mut self,
|
||||
language_server_id: LanguageServerId,
|
||||
registration_id: &str,
|
||||
params: DidChangeWatchedFilesRegistrationOptions,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let registrations = self
|
||||
.language_server_watcher_registrations
|
||||
.entry(language_server_id)
|
||||
.or_default();
|
||||
|
||||
registrations.insert(registration_id.to_string(), params.watchers);
|
||||
|
||||
self.rebuild_watched_paths(language_server_id, cx);
|
||||
}
|
||||
|
||||
fn on_lsp_unregister_did_change_watched_files(
|
||||
&mut self,
|
||||
language_server_id: LanguageServerId,
|
||||
registration_id: &str,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let registrations = self
|
||||
.language_server_watcher_registrations
|
||||
.entry(language_server_id)
|
||||
.or_default();
|
||||
|
||||
if registrations.remove(registration_id).is_some() {
|
||||
log::info!(
|
||||
"language server {}: unregistered workspace/DidChangeWatchedFiles capability with id {}",
|
||||
language_server_id,
|
||||
registration_id
|
||||
);
|
||||
} else {
|
||||
log::warn!(
|
||||
"language server {}: failed to unregister workspace/DidChangeWatchedFiles capability with id {}. not registered.",
|
||||
language_server_id,
|
||||
registration_id
|
||||
);
|
||||
}
|
||||
|
||||
self.rebuild_watched_paths(language_server_id, cx);
|
||||
}
|
||||
|
||||
fn rebuild_watched_paths(
|
||||
&mut self,
|
||||
language_server_id: LanguageServerId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let Some(watchers) = self
|
||||
.language_server_watcher_registrations
|
||||
.get(&language_server_id)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let watched_paths = self
|
||||
.language_server_watched_paths
|
||||
.entry(language_server_id)
|
||||
.or_default();
|
||||
|
||||
let mut builders = HashMap::default();
|
||||
for watcher in watchers.values().flatten() {
|
||||
for watcher in params.watchers {
|
||||
for worktree in &self.worktrees {
|
||||
if let Some(worktree) = worktree.upgrade() {
|
||||
let glob_is_inside_worktree = worktree.update(cx, |tree, _| {
|
||||
@@ -4704,11 +4626,11 @@ impl Project {
|
||||
if self.is_local() {
|
||||
let buffers_with_paths = buffers
|
||||
.into_iter()
|
||||
.map(|buffer_handle| {
|
||||
.filter_map(|buffer_handle| {
|
||||
let buffer = buffer_handle.read(cx);
|
||||
let buffer_abs_path = File::from_dyn(buffer.file())
|
||||
.and_then(|file| file.as_local().map(|f| f.abs_path(cx)));
|
||||
(buffer_handle, buffer_abs_path)
|
||||
let file = File::from_dyn(buffer.file())?;
|
||||
let buffer_abs_path = file.as_local().map(|f| f.abs_path(cx));
|
||||
Some((buffer_handle, buffer_abs_path))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::{btree_map, BTreeMap, VecDeque};
|
||||
use futures::{
|
||||
channel::mpsc::{unbounded, UnboundedSender},
|
||||
@@ -14,17 +13,13 @@ use futures::{
|
||||
};
|
||||
use gpui::{AppContext, Context, Model, ModelContext, Task};
|
||||
use itertools::Itertools;
|
||||
use language::{ContextProvider, Language, Location};
|
||||
use language::Language;
|
||||
use task::{
|
||||
static_source::StaticSource, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates,
|
||||
TaskVariables, VariableName,
|
||||
static_source::StaticSource, ResolvedTask, TaskContext, TaskId, TaskTemplate, VariableName,
|
||||
};
|
||||
use text::{Point, ToPoint};
|
||||
use util::{post_inc, NumericPrefixWithSuffix};
|
||||
use worktree::WorktreeId;
|
||||
|
||||
use crate::Project;
|
||||
|
||||
/// Inventory tracks available tasks for a given project.
|
||||
pub struct Inventory {
|
||||
sources: Vec<SourceInInventory>,
|
||||
@@ -496,102 +491,6 @@ mod test_inventory {
|
||||
}
|
||||
}
|
||||
|
||||
/// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file.
|
||||
/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
|
||||
pub struct BasicContextProvider {
|
||||
project: Model<Project>,
|
||||
}
|
||||
|
||||
impl BasicContextProvider {
|
||||
pub fn new(project: Model<Project>) -> Self {
|
||||
Self { project }
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextProvider for BasicContextProvider {
|
||||
fn build_context(
|
||||
&self,
|
||||
_: &TaskVariables,
|
||||
location: &Location,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<TaskVariables> {
|
||||
let buffer = location.buffer.read(cx);
|
||||
let buffer_snapshot = buffer.snapshot();
|
||||
let symbols = buffer_snapshot.symbols_containing(location.range.start, None);
|
||||
let symbol = symbols.unwrap_or_default().last().map(|symbol| {
|
||||
let range = symbol
|
||||
.name_ranges
|
||||
.last()
|
||||
.cloned()
|
||||
.unwrap_or(0..symbol.text.len());
|
||||
symbol.text[range].to_string()
|
||||
});
|
||||
|
||||
let current_file = buffer
|
||||
.file()
|
||||
.and_then(|file| file.as_local())
|
||||
.map(|file| file.abs_path(cx).to_string_lossy().to_string());
|
||||
let Point { row, column } = location.range.start.to_point(&buffer_snapshot);
|
||||
let row = row + 1;
|
||||
let column = column + 1;
|
||||
let selected_text = buffer
|
||||
.chars_for_range(location.range.clone())
|
||||
.collect::<String>();
|
||||
|
||||
let mut task_variables = TaskVariables::from_iter([
|
||||
(VariableName::Row, row.to_string()),
|
||||
(VariableName::Column, column.to_string()),
|
||||
]);
|
||||
|
||||
if let Some(symbol) = symbol {
|
||||
task_variables.insert(VariableName::Symbol, symbol);
|
||||
}
|
||||
if !selected_text.trim().is_empty() {
|
||||
task_variables.insert(VariableName::SelectedText, selected_text);
|
||||
}
|
||||
if let Some(path) = current_file {
|
||||
task_variables.insert(VariableName::File, path);
|
||||
}
|
||||
|
||||
let worktree_abs_path = buffer
|
||||
.file()
|
||||
.map(|file| WorktreeId::from_usize(file.worktree_id()))
|
||||
.and_then(|worktree_id| {
|
||||
self.project
|
||||
.read(cx)
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
.map(|worktree| worktree.read(cx).abs_path())
|
||||
});
|
||||
if let Some(worktree_path) = worktree_abs_path {
|
||||
task_variables.insert(
|
||||
VariableName::WorktreeRoot,
|
||||
worktree_path.to_string_lossy().to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(task_variables)
|
||||
}
|
||||
}
|
||||
|
||||
/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks.
|
||||
pub struct ContextProviderWithTasks {
|
||||
templates: TaskTemplates,
|
||||
}
|
||||
|
||||
impl ContextProviderWithTasks {
|
||||
pub fn new(definitions: TaskTemplates) -> Self {
|
||||
Self {
|
||||
templates: definitions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextProvider for ContextProviderWithTasks {
|
||||
fn associated_tasks(&self) -> Option<TaskTemplates> {
|
||||
Some(self.templates.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::TestAppContext;
|
||||
|
||||
@@ -3,16 +3,10 @@ use collections::HashMap;
|
||||
use gpui::{
|
||||
AnyWindowHandle, AppContext, Context, Entity, Model, ModelContext, SharedString, WeakModel,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use settings::{Settings, SettingsLocation};
|
||||
use smol::channel::bounded;
|
||||
use std::{
|
||||
env,
|
||||
fs::File,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use task::{SpawnInTerminal, TerminalWorkDir};
|
||||
use std::path::{Path, PathBuf};
|
||||
use task::SpawnInTerminal;
|
||||
use terminal::{
|
||||
terminal_settings::{self, Shell, TerminalSettings, VenvSettingsContent},
|
||||
TaskState, TaskStatus, Terminal, TerminalBuilder,
|
||||
@@ -33,55 +27,58 @@ pub struct ConnectRemoteTerminal {
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn terminal_work_dir_for(
|
||||
pub fn remote_terminal_connection_data(
|
||||
&self,
|
||||
pathbuf: Option<&Path>,
|
||||
cx: &AppContext,
|
||||
) -> Option<TerminalWorkDir> {
|
||||
if self.is_local() {
|
||||
return Some(TerminalWorkDir::Local(pathbuf?.to_owned()));
|
||||
}
|
||||
let dev_server_project_id = self.dev_server_project_id()?;
|
||||
let projects_store = dev_server_projects::Store::global(cx).read(cx);
|
||||
let ssh_command = projects_store
|
||||
.dev_server_for_project(dev_server_project_id)?
|
||||
.ssh_connection_string
|
||||
.as_ref()?
|
||||
.to_string();
|
||||
|
||||
let path = if let Some(pathbuf) = pathbuf {
|
||||
pathbuf.to_string_lossy().to_string()
|
||||
} else {
|
||||
projects_store
|
||||
.dev_server_project(dev_server_project_id)?
|
||||
.path
|
||||
.to_string()
|
||||
};
|
||||
|
||||
Some(TerminalWorkDir::Ssh {
|
||||
ssh_command,
|
||||
path: Some(path),
|
||||
})
|
||||
) -> Option<ConnectRemoteTerminal> {
|
||||
self.dev_server_project_id()
|
||||
.and_then(|dev_server_project_id| {
|
||||
let projects_store = dev_server_projects::Store::global(cx).read(cx);
|
||||
let project_path = projects_store
|
||||
.dev_server_project(dev_server_project_id)?
|
||||
.path
|
||||
.clone();
|
||||
let ssh_connection_string = projects_store
|
||||
.dev_server_for_project(dev_server_project_id)?
|
||||
.ssh_connection_string
|
||||
.clone();
|
||||
Some(project_path).zip(ssh_connection_string)
|
||||
})
|
||||
.map(
|
||||
|(project_path, ssh_connection_string)| ConnectRemoteTerminal {
|
||||
ssh_connection_string,
|
||||
project_path,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn create_terminal(
|
||||
&mut self,
|
||||
working_directory: Option<TerminalWorkDir>,
|
||||
working_directory: Option<PathBuf>,
|
||||
spawn_task: Option<SpawnInTerminal>,
|
||||
window: AnyWindowHandle,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> anyhow::Result<Model<Terminal>> {
|
||||
let remote_connection_data = if self.is_remote() {
|
||||
let remote_connection_data = self.remote_terminal_connection_data(cx);
|
||||
if remote_connection_data.is_none() {
|
||||
anyhow::bail!("Cannot create terminal for remote project without connection data")
|
||||
}
|
||||
remote_connection_data
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// used only for TerminalSettings::get
|
||||
let worktree = {
|
||||
let terminal_cwd = working_directory.as_ref().and_then(|cwd| cwd.local_path());
|
||||
let terminal_cwd = working_directory.as_deref();
|
||||
let task_cwd = spawn_task
|
||||
.as_ref()
|
||||
.and_then(|spawn_task| spawn_task.cwd.as_ref())
|
||||
.and_then(|cwd| cwd.local_path());
|
||||
.and_then(|spawn_task| spawn_task.cwd.as_deref());
|
||||
|
||||
terminal_cwd
|
||||
.and_then(|terminal_cwd| self.find_local_worktree(&terminal_cwd, cx))
|
||||
.or_else(|| task_cwd.and_then(|spawn_cwd| self.find_local_worktree(&spawn_cwd, cx)))
|
||||
.and_then(|terminal_cwd| self.find_local_worktree(terminal_cwd, cx))
|
||||
.or_else(|| task_cwd.and_then(|spawn_cwd| self.find_local_worktree(spawn_cwd, cx)))
|
||||
};
|
||||
|
||||
let settings_location = worktree.as_ref().map(|(worktree, path)| SettingsLocation {
|
||||
@@ -89,8 +86,7 @@ impl Project {
|
||||
path,
|
||||
});
|
||||
|
||||
let is_terminal = spawn_task.is_none() && (working_directory.as_ref().is_none())
|
||||
|| (working_directory.as_ref().unwrap().is_local());
|
||||
let is_terminal = spawn_task.is_none() && remote_connection_data.is_none();
|
||||
let settings = TerminalSettings::get(settings_location, cx);
|
||||
let python_settings = settings.detect_venv.clone();
|
||||
let (completion_tx, completion_rx) = bounded(1);
|
||||
@@ -99,80 +95,64 @@ impl Project {
|
||||
// Alacritty uses parent project's working directory when no working directory is provided
|
||||
// https://github.com/alacritty/alacritty/blob/fd1a3cc79192d1d03839f0fd8c72e1f8d0fce42e/extra/man/alacritty.5.scd?plain=1#L47-L52
|
||||
|
||||
let mut retained_script = None;
|
||||
|
||||
let venv_base_directory = working_directory
|
||||
.as_ref()
|
||||
.and_then(|cwd| cwd.local_path())
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| Path::new(""));
|
||||
|
||||
let (spawn_task, shell) = match working_directory.as_ref() {
|
||||
Some(TerminalWorkDir::Ssh { ssh_command, path }) => {
|
||||
log::debug!("Connecting to a remote server: {ssh_command:?}");
|
||||
let tmp_dir = tempfile::tempdir()?;
|
||||
let ssh_shell_result = prepare_ssh_shell(
|
||||
&mut env,
|
||||
tmp_dir.path(),
|
||||
spawn_task.as_ref(),
|
||||
ssh_command,
|
||||
path.as_deref(),
|
||||
);
|
||||
retained_script = Some(tmp_dir);
|
||||
let ssh_shell = ssh_shell_result?;
|
||||
let (spawn_task, shell) = if let Some(remote_connection_data) = remote_connection_data {
|
||||
log::debug!("Connecting to a remote server: {remote_connection_data:?}");
|
||||
// Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
|
||||
// to properly display colors.
|
||||
// We do not have the luxury of assuming the host has it installed,
|
||||
// so we set it to a default that does not break the highlighting via ssh.
|
||||
env.entry("TERM".to_string())
|
||||
.or_insert_with(|| "xterm-256color".to_string());
|
||||
|
||||
(
|
||||
spawn_task.map(|spawn_task| TaskState {
|
||||
id: spawn_task.id,
|
||||
full_label: spawn_task.full_label,
|
||||
label: spawn_task.label,
|
||||
command_label: spawn_task.command_label,
|
||||
status: TaskStatus::Running,
|
||||
completion_rx,
|
||||
}),
|
||||
ssh_shell,
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
if let Some(spawn_task) = spawn_task {
|
||||
log::debug!("Spawning task: {spawn_task:?}");
|
||||
env.extend(spawn_task.env);
|
||||
// Activate minimal Python virtual environment
|
||||
if let Some(python_settings) = &python_settings.as_option() {
|
||||
self.set_python_venv_path_for_tasks(
|
||||
python_settings,
|
||||
&venv_base_directory,
|
||||
&mut env,
|
||||
);
|
||||
}
|
||||
(
|
||||
Some(TaskState {
|
||||
id: spawn_task.id,
|
||||
full_label: spawn_task.full_label,
|
||||
label: spawn_task.label,
|
||||
command_label: spawn_task.command_label,
|
||||
status: TaskStatus::Running,
|
||||
completion_rx,
|
||||
}),
|
||||
Shell::WithArguments {
|
||||
program: spawn_task.command,
|
||||
args: spawn_task.args,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(None, settings.shell.clone())
|
||||
}
|
||||
(
|
||||
None,
|
||||
Shell::WithArguments {
|
||||
program: "ssh".to_string(),
|
||||
args: vec![
|
||||
remote_connection_data.ssh_connection_string.to_string(),
|
||||
"-t".to_string(),
|
||||
format!(
|
||||
"cd {} && exec $SHELL -l",
|
||||
escape_path_for_shell(remote_connection_data.project_path.as_ref())
|
||||
),
|
||||
],
|
||||
},
|
||||
)
|
||||
} else if let Some(spawn_task) = spawn_task {
|
||||
log::debug!("Spawning task: {spawn_task:?}");
|
||||
env.extend(spawn_task.env);
|
||||
// Activate minimal Python virtual environment
|
||||
if let Some(python_settings) = &python_settings.as_option() {
|
||||
self.set_python_venv_path_for_tasks(python_settings, venv_base_directory, &mut env);
|
||||
}
|
||||
(
|
||||
Some(TaskState {
|
||||
id: spawn_task.id,
|
||||
full_label: spawn_task.full_label,
|
||||
label: spawn_task.label,
|
||||
command_label: spawn_task.command_label,
|
||||
status: TaskStatus::Running,
|
||||
completion_rx,
|
||||
}),
|
||||
Shell::WithArguments {
|
||||
program: spawn_task.command,
|
||||
args: spawn_task.args,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(None, settings.shell.clone())
|
||||
};
|
||||
|
||||
let terminal = TerminalBuilder::new(
|
||||
working_directory
|
||||
.as_ref()
|
||||
.and_then(|cwd| cwd.local_path())
|
||||
.map(ToOwned::to_owned),
|
||||
working_directory.clone(),
|
||||
spawn_task,
|
||||
shell,
|
||||
env,
|
||||
Some(settings.blinking),
|
||||
Some(settings.blinking.clone()),
|
||||
settings.alternate_scroll,
|
||||
settings.max_scroll_history_lines,
|
||||
window,
|
||||
@@ -187,7 +167,6 @@ impl Project {
|
||||
|
||||
let id = terminal_handle.entity_id();
|
||||
cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
|
||||
drop(retained_script);
|
||||
let handles = &mut project.terminals.local_handles;
|
||||
|
||||
if let Some(index) = handles
|
||||
@@ -204,7 +183,7 @@ impl Project {
|
||||
if is_terminal {
|
||||
if let Some(python_settings) = &python_settings.as_option() {
|
||||
if let Some(activate_script_path) =
|
||||
self.find_activate_script_path(python_settings, &venv_base_directory)
|
||||
self.find_activate_script_path(python_settings, venv_base_directory)
|
||||
{
|
||||
self.activate_python_virtual_environment(
|
||||
Project::get_activate_command(python_settings),
|
||||
@@ -313,83 +292,38 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_ssh_shell(
|
||||
env: &mut HashMap<String, String>,
|
||||
tmp_dir: &Path,
|
||||
spawn_task: Option<&SpawnInTerminal>,
|
||||
ssh_command: &str,
|
||||
path: Option<&str>,
|
||||
) -> anyhow::Result<Shell> {
|
||||
// Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
|
||||
// to properly display colors.
|
||||
// We do not have the luxury of assuming the host has it installed,
|
||||
// so we set it to a default that does not break the highlighting via ssh.
|
||||
env.entry("TERM".to_string())
|
||||
.or_insert_with(|| "xterm-256color".to_string());
|
||||
|
||||
let real_ssh = which::which("ssh")?;
|
||||
let ssh_path = tmp_dir.join("ssh");
|
||||
let mut ssh_file = File::create(&ssh_path)?;
|
||||
|
||||
let to_run = if let Some(spawn_task) = spawn_task {
|
||||
Some(shlex::try_quote(&spawn_task.command)?)
|
||||
.into_iter()
|
||||
.chain(
|
||||
spawn_task
|
||||
.args
|
||||
.iter()
|
||||
.filter_map(|arg| shlex::try_quote(arg).ok()),
|
||||
)
|
||||
.join(" ")
|
||||
} else {
|
||||
"exec $SHELL -l".to_string()
|
||||
};
|
||||
|
||||
let (port_forward, local_dev_env) =
|
||||
if env::var("ZED_RPC_URL").as_deref() == Ok("http://localhost:8080/rpc") {
|
||||
(
|
||||
"-R 8080:localhost:8080",
|
||||
"export ZED_RPC_URL=http://localhost:8080/rpc;",
|
||||
)
|
||||
} else {
|
||||
("", "")
|
||||
};
|
||||
|
||||
let commands = if let Some(path) = path {
|
||||
// I've found that `ssh -t dev sh -c 'cd; cd /tmp; pwd'` gives /tmp
|
||||
// but `ssh -t dev sh -c 'cd /tmp; pwd'` gives /root
|
||||
format!("cd {path}; {local_dev_env} {to_run}")
|
||||
} else {
|
||||
format!("cd; {local_dev_env} {to_run}")
|
||||
};
|
||||
let shell_invocation = &format!("sh -c {}", shlex::try_quote(&commands)?);
|
||||
|
||||
// To support things like `gh cs ssh`/`coder ssh`, we run whatever command
|
||||
// you have configured, but place our custom script on the path so that it will
|
||||
// be run instead.
|
||||
write!(
|
||||
&mut ssh_file,
|
||||
"#!/bin/sh\nexec {} \"$@\" {} {} {}",
|
||||
real_ssh.to_string_lossy(),
|
||||
if spawn_task.is_none() { "-t" } else { "" },
|
||||
port_forward,
|
||||
shlex::try_quote(shell_invocation)?,
|
||||
)?;
|
||||
|
||||
// todo(windows)
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
std::fs::set_permissions(ssh_path, smol::fs::unix::PermissionsExt::from_mode(0o755))?;
|
||||
let path = format!(
|
||||
"{}:{}",
|
||||
tmp_dir.to_string_lossy(),
|
||||
env.get("PATH")
|
||||
.cloned()
|
||||
.or(env::var("PATH").ok())
|
||||
.unwrap_or_default()
|
||||
);
|
||||
env.insert("PATH".to_string(), path);
|
||||
|
||||
let mut args = shlex::split(&ssh_command).unwrap_or_default();
|
||||
let program = args.drain(0..1).next().unwrap_or("ssh".to_string());
|
||||
Ok(Shell::WithArguments { program, args })
|
||||
#[cfg(unix)]
|
||||
fn escape_path_for_shell(input: &str) -> String {
|
||||
input
|
||||
.chars()
|
||||
.fold(String::with_capacity(input.len()), |mut s, c| {
|
||||
match c {
|
||||
' ' | '"' | '\'' | '\\' | '(' | ')' | '{' | '}' | '[' | ']' | '|' | ';' | '&'
|
||||
| '<' | '>' | '*' | '?' | '$' | '#' | '!' | '=' | '^' | '%' | ':' => {
|
||||
s.push('\\');
|
||||
s.push('\\');
|
||||
s.push(c);
|
||||
}
|
||||
_ => s.push(c),
|
||||
}
|
||||
s
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn escape_path_for_shell(input: &str) -> String {
|
||||
input
|
||||
.chars()
|
||||
.fold(String::with_capacity(input.len()), |mut s, c| {
|
||||
match c {
|
||||
'^' | '&' | '|' | '<' | '>' | ' ' | '(' | ')' | '@' | '`' | '=' | ';' | '%' => {
|
||||
s.push('^');
|
||||
s.push(c);
|
||||
}
|
||||
_ => s.push(c),
|
||||
}
|
||||
s
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Add a few tests for adding and removing terminal tabs
|
||||
|
||||
@@ -1685,13 +1685,7 @@ impl ProjectPanel {
|
||||
let filename_text_color =
|
||||
entry_git_aware_label_color(details.git_status, details.is_ignored, is_selected);
|
||||
let file_name = details.filename.clone();
|
||||
let mut icon = details.icon.clone();
|
||||
if show_editor && details.kind.is_file() {
|
||||
let filename = self.filename_editor.read(cx).text(cx);
|
||||
if filename.len() > 2 {
|
||||
icon = FileIcons::get_icon(Path::new(&filename), cx);
|
||||
}
|
||||
}
|
||||
let icon = details.icon.clone();
|
||||
let depth = details.depth;
|
||||
div()
|
||||
.id(entry_id.to_proto() as usize)
|
||||
|
||||
@@ -14,7 +14,6 @@ doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fuzzy.workspace = true
|
||||
@@ -27,8 +26,6 @@ dev_server_projects.workspace = true
|
||||
rpc.workspace = true
|
||||
serde.workspace = true
|
||||
smol.workspace = true
|
||||
task.workspace = true
|
||||
terminal_view.workspace = true
|
||||
ui.workspace = true
|
||||
ui_text_field.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,5 @@
|
||||
mod dev_servers;
|
||||
|
||||
use client::ProjectId;
|
||||
use dev_servers::reconnect_to_dev_server;
|
||||
pub use dev_servers::DevServerProjects;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
@@ -19,7 +17,6 @@ use serde::Deserialize;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use ui::{
|
||||
prelude::*, tooltip_container, ButtonLike, IconWithIndicator, Indicator, KeyBinding, ListItem,
|
||||
@@ -316,59 +313,72 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
}
|
||||
}
|
||||
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
|
||||
let store = dev_server_projects::Store::global(cx);
|
||||
let Some(project_id) = store.read(cx)
|
||||
let store = dev_server_projects::Store::global(cx).read(cx);
|
||||
let Some(project_id) = store
|
||||
.dev_server_project(dev_server_project.id)
|
||||
.and_then(|p| p.project_id)
|
||||
else {
|
||||
let server = store.read(cx).dev_server_for_project(dev_server_project.id);
|
||||
if server.is_some_and(|server| server.ssh_connection_string.is_some()) {
|
||||
let reconnect = reconnect_to_dev_server(cx.view().clone(), server.unwrap().clone(), cx);
|
||||
let id = dev_server_project.id;
|
||||
return cx.spawn(|workspace, mut cx| async move {
|
||||
reconnect.await?;
|
||||
|
||||
cx.background_executor().timer(Duration::from_millis(1000)).await;
|
||||
|
||||
if let Some(project_id) = store.update(&mut cx, |store, _| {
|
||||
store.dev_server_project(id)
|
||||
.and_then(|p| p.project_id)
|
||||
})? {
|
||||
workspace.update(&mut cx, move |_, cx| {
|
||||
open_dev_server_project(replace_current_window, project_id, cx)
|
||||
})?.await?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
let dev_server_name = dev_server_project.dev_server_name.clone();
|
||||
return cx.spawn(|workspace, mut cx| async move {
|
||||
let response =
|
||||
cx.prompt(gpui::PromptLevel::Warning,
|
||||
"Dev Server is offline",
|
||||
Some(format!("Cannot connect to {}. To debug open the remote project settings.", dev_server_name).as_str()),
|
||||
&["Ok", "Open Settings"]
|
||||
).await?;
|
||||
if response == 1 {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.toggle_modal(cx, |cx| DevServerProjects::new(cx))
|
||||
})?;
|
||||
} else {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
RecentProjects::open(workspace, true, cx);
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
};
|
||||
if let Some(app_state) = AppState::global(cx).upgrade() {
|
||||
let handle = if replace_current_window {
|
||||
cx.window_handle().downcast::<Workspace>()
|
||||
} else {
|
||||
let dev_server_name = dev_server_project.dev_server_name.clone();
|
||||
return cx.spawn(|workspace, mut cx| async move {
|
||||
let response =
|
||||
cx.prompt(gpui::PromptLevel::Warning,
|
||||
"Dev Server is offline",
|
||||
Some(format!("Cannot connect to {}. To debug open the remote project settings.", dev_server_name).as_str()),
|
||||
&["Ok", "Open Settings"]
|
||||
).await?;
|
||||
if response == 1 {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let handle = cx.view().downgrade();
|
||||
workspace.toggle_modal(cx, |cx| DevServerProjects::new(cx, handle))
|
||||
})?;
|
||||
} else {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
RecentProjects::open(workspace, true, cx);
|
||||
})?;
|
||||
}
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(handle) = handle {
|
||||
cx.spawn(move |workspace, mut cx| async move {
|
||||
let continue_replacing = workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.
|
||||
prepare_to_close(true, cx)
|
||||
})?
|
||||
.await?;
|
||||
if continue_replacing {
|
||||
workspace
|
||||
.update(&mut cx, |_workspace, cx| {
|
||||
workspace::join_dev_server_project(project_id, app_state, Some(handle), cx)
|
||||
})?
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
else {
|
||||
let task =
|
||||
workspace::join_dev_server_project(project_id, app_state, None, cx);
|
||||
cx.spawn(|_, _| async move {
|
||||
task.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
};
|
||||
open_dev_server_project(replace_current_window, project_id, cx)
|
||||
} else {
|
||||
Task::ready(Err(anyhow::anyhow!("App state not found")))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
.detach_and_log_err(cx);
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
@@ -535,7 +545,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
.when_some(KeyBinding::for_action(&OpenRemote, cx), |button, key| {
|
||||
button.child(key)
|
||||
})
|
||||
.child(Label::new("New remote project…").color(Color::Muted))
|
||||
.child(Label::new("Connect…").color(Color::Muted))
|
||||
.on_click(|_, cx| cx.dispatch_action(OpenRemote.boxed_clone())),
|
||||
)
|
||||
.child(
|
||||
@@ -544,7 +554,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
KeyBinding::for_action(&workspace::Open, cx),
|
||||
|button, key| button.child(key),
|
||||
)
|
||||
.child(Label::new("Open local folder…").color(Color::Muted))
|
||||
.child(Label::new("Open folder…").color(Color::Muted))
|
||||
.on_click(|_, cx| cx.dispatch_action(workspace::Open.boxed_clone())),
|
||||
)
|
||||
.into_any(),
|
||||
@@ -552,51 +562,6 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn open_dev_server_project(
|
||||
replace_current_window: bool,
|
||||
project_id: ProjectId,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
if let Some(app_state) = AppState::global(cx).upgrade() {
|
||||
let handle = if replace_current_window {
|
||||
cx.window_handle().downcast::<Workspace>()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(handle) = handle {
|
||||
cx.spawn(move |workspace, mut cx| async move {
|
||||
let continue_replacing = workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.prepare_to_close(true, cx)
|
||||
})?
|
||||
.await?;
|
||||
if continue_replacing {
|
||||
workspace
|
||||
.update(&mut cx, |_workspace, cx| {
|
||||
workspace::join_dev_server_project(
|
||||
project_id,
|
||||
app_state,
|
||||
Some(handle),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
} else {
|
||||
let task = workspace::join_dev_server_project(project_id, app_state, None, cx);
|
||||
cx.spawn(|_, _| async move {
|
||||
task.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Task::ready(Err(anyhow::anyhow!("App state not found")))
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the highlighted text for the name and path
|
||||
fn highlights_for_path(
|
||||
path: &Path,
|
||||
|
||||
@@ -515,7 +515,6 @@ message ShutdownDevServer {
|
||||
message RenameDevServer {
|
||||
uint64 dev_server_id = 1;
|
||||
string name = 2;
|
||||
optional string ssh_connection_string = 3;
|
||||
}
|
||||
|
||||
message DeleteDevServer {
|
||||
|
||||
@@ -42,9 +42,13 @@ const MIN_INPUT_WIDTH_REMS: f32 = 10.;
|
||||
const MAX_INPUT_WIDTH_REMS: f32 = 30.;
|
||||
const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;
|
||||
|
||||
const fn true_value() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize)]
|
||||
pub struct Deploy {
|
||||
#[serde(default = "util::serde::default_true")]
|
||||
#[serde(default = "true_value")]
|
||||
pub focus: bool,
|
||||
#[serde(default)]
|
||||
pub replace_enabled: bool,
|
||||
|
||||
@@ -49,7 +49,6 @@ where
|
||||
&self.position
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn end(&self, cx: &<T::Summary as Summary>::Context) -> D {
|
||||
if let Some(item_summary) = self.item_summary() {
|
||||
let mut end = self.start().clone();
|
||||
@@ -60,7 +59,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn item(&self) -> Option<&'a T> {
|
||||
self.assert_did_seek();
|
||||
if let Some(entry) = self.stack.last() {
|
||||
@@ -79,7 +77,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn item_summary(&self) -> Option<&'a T::Summary> {
|
||||
self.assert_did_seek();
|
||||
if let Some(entry) = self.stack.last() {
|
||||
@@ -100,7 +97,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn next_item(&self) -> Option<&'a T> {
|
||||
self.assert_did_seek();
|
||||
if let Some(entry) = self.stack.last() {
|
||||
@@ -123,7 +119,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn next_leaf(&self) -> Option<&'a SumTree<T>> {
|
||||
for entry in self.stack.iter().rev().skip(1) {
|
||||
if entry.index < entry.tree.0.child_trees().len() - 1 {
|
||||
@@ -138,7 +133,6 @@ where
|
||||
None
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn prev_item(&self) -> Option<&'a T> {
|
||||
self.assert_did_seek();
|
||||
if let Some(entry) = self.stack.last() {
|
||||
@@ -161,7 +155,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn prev_leaf(&self) -> Option<&'a SumTree<T>> {
|
||||
for entry in self.stack.iter().rev().skip(1) {
|
||||
if entry.index != 0 {
|
||||
@@ -176,12 +169,10 @@ where
|
||||
None
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn prev(&mut self, cx: &<T::Summary as Summary>::Context) {
|
||||
self.prev_internal(|_| true, cx)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn prev_internal<F>(&mut self, mut filter_node: F, cx: &<T::Summary as Summary>::Context)
|
||||
where
|
||||
F: FnMut(&T::Summary) -> bool,
|
||||
@@ -247,12 +238,10 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn next(&mut self, cx: &<T::Summary as Summary>::Context) {
|
||||
self.next_internal(|_| true, cx)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn next_internal<F>(&mut self, mut filter_node: F, cx: &<T::Summary as Summary>::Context)
|
||||
where
|
||||
F: FnMut(&T::Summary) -> bool,
|
||||
@@ -340,7 +329,6 @@ where
|
||||
debug_assert!(self.stack.is_empty() || self.stack.last().unwrap().tree.0.is_leaf());
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_did_seek(&self) {
|
||||
assert!(
|
||||
self.did_seek,
|
||||
@@ -354,7 +342,6 @@ where
|
||||
T: Item,
|
||||
D: Dimension<'a, T::Summary>,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn seek<Target>(
|
||||
&mut self,
|
||||
pos: &Target,
|
||||
@@ -368,7 +355,6 @@ where
|
||||
self.seek_internal(pos, bias, &mut (), cx)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn seek_forward<Target>(
|
||||
&mut self,
|
||||
pos: &Target,
|
||||
@@ -381,7 +367,6 @@ where
|
||||
self.seek_internal(pos, bias, &mut (), cx)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn slice<Target>(
|
||||
&mut self,
|
||||
end: &Target,
|
||||
@@ -401,12 +386,10 @@ where
|
||||
slice.tree
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn suffix(&mut self, cx: &<T::Summary as Summary>::Context) -> SumTree<T> {
|
||||
self.slice(&End::new(), Bias::Right, cx)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn summary<Target, Output>(
|
||||
&mut self,
|
||||
end: &Target,
|
||||
@@ -423,7 +406,6 @@ where
|
||||
}
|
||||
|
||||
/// Returns whether we found the item you where seeking for
|
||||
#[track_caller]
|
||||
fn seek_internal(
|
||||
&mut self,
|
||||
target: &dyn SeekTarget<'a, T::Summary, D>,
|
||||
|
||||
@@ -5,12 +5,11 @@ pub mod static_source;
|
||||
mod task_template;
|
||||
mod vscode_format;
|
||||
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use collections::{HashMap, HashSet};
|
||||
use gpui::SharedString;
|
||||
use serde::Serialize;
|
||||
use std::borrow::Cow;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::{borrow::Cow, path::Path};
|
||||
|
||||
pub use task_template::{RevealStrategy, TaskTemplate, TaskTemplates};
|
||||
pub use vscode_format::VsCodeTaskFile;
|
||||
@@ -20,38 +19,6 @@ pub use vscode_format::VsCodeTaskFile;
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct TaskId(pub String);
|
||||
|
||||
/// TerminalWorkDir describes where a task should be run
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TerminalWorkDir {
|
||||
/// Local is on this machine
|
||||
Local(PathBuf),
|
||||
/// SSH runs the terminal over ssh
|
||||
Ssh {
|
||||
/// The command to run to connect
|
||||
ssh_command: String,
|
||||
/// The path on the remote server
|
||||
path: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl TerminalWorkDir {
|
||||
/// Returns whether the terminal task is supposed to be spawned on a local machine or not.
|
||||
pub fn is_local(&self) -> bool {
|
||||
match self {
|
||||
Self::Local(_) => true,
|
||||
Self::Ssh { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a local CWD if the terminal is local, None otherwise.
|
||||
pub fn local_path(&self) -> Option<&Path> {
|
||||
match self {
|
||||
Self::Local(path) => Some(path),
|
||||
Self::Ssh { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains all information needed by Zed to spawn a new terminal tab for the given task.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SpawnInTerminal {
|
||||
@@ -69,7 +36,7 @@ pub struct SpawnInTerminal {
|
||||
/// A human-readable label, containing command and all of its arguments, joined and substituted.
|
||||
pub command_label: String,
|
||||
/// Current working directory to spawn the command into.
|
||||
pub cwd: Option<TerminalWorkDir>,
|
||||
pub cwd: Option<PathBuf>,
|
||||
/// Env overrides for the command, will be appended to the terminal's environment from the settings.
|
||||
pub env: HashMap<String, String>,
|
||||
/// Whether to use a new terminal tab or reuse the existing one to spawn the process.
|
||||
@@ -121,7 +88,7 @@ impl ResolvedTask {
|
||||
}
|
||||
|
||||
/// Variables, available for use in [`TaskContext`] when a Zed's [`TaskTemplate`] gets resolved into a [`ResolvedTask`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
|
||||
pub enum VariableName {
|
||||
/// An absolute path of the currently opened file.
|
||||
File,
|
||||
@@ -135,6 +102,8 @@ pub enum VariableName {
|
||||
Column,
|
||||
/// Text from the latest selection.
|
||||
SelectedText,
|
||||
/// The symbol selected by the symbol tagging system, specifically the @run capture in a runnables.scm
|
||||
RunnableSymbol,
|
||||
/// Custom variable, provided by the plugin or other external source.
|
||||
/// Will be printed with `ZED_` prefix to avoid potential conflicts with other variables.
|
||||
Custom(Cow<'static, str>),
|
||||
@@ -164,6 +133,7 @@ impl std::fmt::Display for VariableName {
|
||||
Self::Row => write!(f, "{ZED_VARIABLE_NAME_PREFIX}ROW"),
|
||||
Self::Column => write!(f, "{ZED_VARIABLE_NAME_PREFIX}COLUMN"),
|
||||
Self::SelectedText => write!(f, "{ZED_VARIABLE_NAME_PREFIX}SELECTED_TEXT"),
|
||||
Self::RunnableSymbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RUNNABLE_SYMBOL"),
|
||||
Self::Custom(s) => write!(f, "{ZED_VARIABLE_NAME_PREFIX}CUSTOM_{s}"),
|
||||
}
|
||||
}
|
||||
@@ -171,7 +141,7 @@ impl std::fmt::Display for VariableName {
|
||||
|
||||
/// Container for predefined environment variables that describe state of Zed at the time the task was spawned.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct TaskVariables(BTreeMap<VariableName, String>);
|
||||
pub struct TaskVariables(HashMap<VariableName, String>);
|
||||
|
||||
impl TaskVariables {
|
||||
/// Inserts another variable into the container, overwriting the existing one if it already exists — in this case, the old value is returned.
|
||||
@@ -183,56 +153,14 @@ impl TaskVariables {
|
||||
pub fn extend(&mut self, other: Self) {
|
||||
self.0.extend(other.0);
|
||||
}
|
||||
/// Get the value associated with given variable name, if there is one.
|
||||
pub fn get(&self, key: &VariableName) -> Option<&str> {
|
||||
self.0.get(key).map(|s| s.as_str())
|
||||
}
|
||||
/// Clear out variables obtained from tree-sitter queries, which are prefixed with '_' character
|
||||
pub fn sweep(&mut self) {
|
||||
self.0.retain(|name, _| {
|
||||
if let VariableName::Custom(name) = name {
|
||||
!name.starts_with('_')
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns iterator over names of all set task variables.
|
||||
pub fn keys(&self) -> impl Iterator<Item = &VariableName> {
|
||||
self.0.keys()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(VariableName, String)> for TaskVariables {
|
||||
fn from_iter<T: IntoIterator<Item = (VariableName, String)>>(iter: T) -> Self {
|
||||
Self(BTreeMap::from_iter(iter))
|
||||
Self(HashMap::from_iter(iter))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for VariableName {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let without_prefix = s.strip_prefix(ZED_VARIABLE_NAME_PREFIX).ok_or(())?;
|
||||
let value = match without_prefix {
|
||||
"FILE" => Self::File,
|
||||
"WORKTREE_ROOT" => Self::WorktreeRoot,
|
||||
"SYMBOL" => Self::Symbol,
|
||||
"SELECTED_TEXT" => Self::SelectedText,
|
||||
"ROW" => Self::Row,
|
||||
"COLUMN" => Self::Column,
|
||||
_ => {
|
||||
if let Some(custom_name) = without_prefix.strip_prefix("CUSTOM_") {
|
||||
Self::Custom(Cow::Owned(custom_name.to_owned()))
|
||||
} else {
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
/// Keeps track of the file associated with a task and context of tasks execution (i.e. current file or current function).
|
||||
/// Keeps all Zed-related state inside, used to produce a resolved task out of its template.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
|
||||
@@ -8,8 +8,7 @@ use sha2::{Digest, Sha256};
|
||||
use util::{truncate_and_remove_front, ResultExt};
|
||||
|
||||
use crate::{
|
||||
ResolvedTask, SpawnInTerminal, TaskContext, TaskId, TerminalWorkDir, VariableName,
|
||||
ZED_VARIABLE_NAME_PREFIX,
|
||||
ResolvedTask, SpawnInTerminal, TaskContext, TaskId, VariableName, ZED_VARIABLE_NAME_PREFIX,
|
||||
};
|
||||
|
||||
/// A template definition of a Zed task to run.
|
||||
@@ -113,14 +112,12 @@ impl TaskTemplate {
|
||||
&variable_names,
|
||||
&mut substituted_variables,
|
||||
)?;
|
||||
Some(TerminalWorkDir::Local(PathBuf::from(substitured_cwd)))
|
||||
Some(substitured_cwd)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
.or(cx
|
||||
.cwd
|
||||
.as_ref()
|
||||
.map(|cwd| TerminalWorkDir::Local(cwd.clone())));
|
||||
.map(PathBuf::from)
|
||||
.or(cx.cwd.clone());
|
||||
let human_readable_label = substitute_all_template_variables_in_str(
|
||||
&self.label,
|
||||
&truncated_variables,
|
||||
@@ -382,10 +379,7 @@ mod tests {
|
||||
task_variables: TaskVariables::default(),
|
||||
};
|
||||
assert_eq!(
|
||||
resolved_task(&task_without_cwd, &cx)
|
||||
.cwd
|
||||
.as_ref()
|
||||
.and_then(|cwd| cwd.local_path()),
|
||||
resolved_task(&task_without_cwd, &cx).cwd.as_deref(),
|
||||
Some(context_cwd.as_path()),
|
||||
"TaskContext's cwd should be taken on resolve if task's cwd is None"
|
||||
);
|
||||
@@ -400,10 +394,7 @@ mod tests {
|
||||
task_variables: TaskVariables::default(),
|
||||
};
|
||||
assert_eq!(
|
||||
resolved_task(&task_with_cwd, &cx)
|
||||
.cwd
|
||||
.as_ref()
|
||||
.and_then(|cwd| cwd.local_path()),
|
||||
resolved_task(&task_with_cwd, &cx).cwd.as_deref(),
|
||||
Some(task_cwd.as_path()),
|
||||
"TaskTemplate's cwd should be taken on resolve if TaskContext's cwd is None"
|
||||
);
|
||||
@@ -413,10 +404,7 @@ mod tests {
|
||||
task_variables: TaskVariables::default(),
|
||||
};
|
||||
assert_eq!(
|
||||
resolved_task(&task_with_cwd, &cx)
|
||||
.cwd
|
||||
.as_ref()
|
||||
.and_then(|cwd| cwd.local_path()),
|
||||
resolved_task(&task_with_cwd, &cx).cwd.as_deref(),
|
||||
Some(task_cwd.as_path()),
|
||||
"TaskTemplate's cwd should be taken on resolve if TaskContext's cwd is not None"
|
||||
);
|
||||
|
||||
@@ -14,11 +14,9 @@ file_icons.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
menu.workspace = true
|
||||
parking_lot.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
task.workspace = true
|
||||
text.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
|
||||
@@ -10,7 +10,6 @@ use workspace::tasks::schedule_task;
|
||||
use workspace::{tasks::schedule_resolved_task, Workspace};
|
||||
|
||||
mod modal;
|
||||
mod modal_completions;
|
||||
mod settings;
|
||||
|
||||
pub use modal::Spawn;
|
||||
@@ -154,8 +153,8 @@ mod tests {
|
||||
|
||||
use editor::Editor;
|
||||
use gpui::{Entity, TestAppContext};
|
||||
use language::{Language, LanguageConfig};
|
||||
use project::{BasicContextProvider, FakeFs, Project};
|
||||
use language::{BasicContextProvider, Language, LanguageConfig};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use task::{TaskContext, TaskVariables, VariableName};
|
||||
use ui::VisualContext;
|
||||
@@ -192,7 +191,7 @@ mod tests {
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
|
||||
|
||||
let rust_language = Arc::new(
|
||||
Language::new(
|
||||
LanguageConfig::default(),
|
||||
@@ -204,7 +203,7 @@ mod tests {
|
||||
name: (_) @name) @item"#,
|
||||
)
|
||||
.unwrap()
|
||||
.with_context_provider(Some(Arc::new(BasicContextProvider::new(project.clone())))),
|
||||
.with_context_provider(Some(Arc::new(BasicContextProvider))),
|
||||
);
|
||||
|
||||
let typescript_language = Arc::new(
|
||||
@@ -222,9 +221,9 @@ mod tests {
|
||||
")" @context)) @item"#,
|
||||
)
|
||||
.unwrap()
|
||||
.with_context_provider(Some(Arc::new(BasicContextProvider::new(project.clone())))),
|
||||
.with_context_provider(Some(Arc::new(BasicContextProvider))),
|
||||
);
|
||||
|
||||
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
|
||||
let worktree_id = project.update(cx, |project, cx| {
|
||||
project.worktrees().next().unwrap().read(cx).id()
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{active_item_selection_properties, modal_completions::TaskVariablesCompletionProvider};
|
||||
use crate::active_item_selection_properties;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
impl_actions, rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusableView,
|
||||
@@ -139,14 +139,11 @@ impl TasksModal {
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let provider = TaskVariablesCompletionProvider::new(task_context.task_variables.clone());
|
||||
|
||||
let picker = cx.new_view(|cx| {
|
||||
Picker::uniform_list(
|
||||
TasksModalDelegate::new(inventory, task_context, workspace),
|
||||
cx,
|
||||
)
|
||||
.with_completions_provider(Box::new(provider), cx)
|
||||
});
|
||||
let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
@@ -543,8 +540,8 @@ mod tests {
|
||||
|
||||
use editor::Editor;
|
||||
use gpui::{TestAppContext, VisualTestContext};
|
||||
use language::{Language, LanguageConfig, LanguageMatcher, Point};
|
||||
use project::{ContextProviderWithTasks, FakeFs, Project};
|
||||
use language::{ContextProviderWithTasks, Language, LanguageConfig, LanguageMatcher, Point};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use task::TaskTemplates;
|
||||
use workspace::CloseInactiveTabsAndPanes;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user