Compare commits

..

1 Commits

Author SHA1 Message Date
Thorsten Ball
2e3d25b1e0 node runtime: Fix node not being added to PATH 2024-05-21 17:17:14 +02:00
147 changed files with 2891 additions and 7608 deletions

191
Cargo.lock generated
View File

@@ -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]]

View File

@@ -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 = [

View File

@@ -501,12 +501,6 @@
"tab": "editor::ConfirmCompletion"
}
},
{
"context": "Editor && inline_completion && !showing_completions",
"bindings": {
"tab": "editor::AcceptInlineCompletion"
}
},
{
"context": "Editor && showing_code_actions",
"bindings": {

View File

@@ -515,12 +515,6 @@
"tab": "editor::ConfirmCompletion"
}
},
{
"context": "Editor && inline_completion && !showing_completions",
"bindings": {
"tab": "editor::AcceptInlineCompletion"
}
},
{
"context": "Editor && showing_code_actions",
"bindings": {

View File

@@ -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",

View File

@@ -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
}

View File

@@ -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

View File

@@ -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`].

View File

@@ -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(),
})
}
}

View File

@@ -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

View File

@@ -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>,

View File

@@ -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!(),
}
}

View File

@@ -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:?}"
);
}
}

View 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()
}
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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(())
}
}

View File

@@ -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())),
)
}
}

View File

@@ -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
}
}

View File

@@ -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)),
}
}
}

View File

@@ -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(),
}
}
}

View File

@@ -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(),
}
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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;

View File

@@ -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,
)
})

View File

@@ -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 {

View File

@@ -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}");

View File

@@ -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(())

View File

@@ -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

View File

@@ -143,7 +143,6 @@ gpui::actions!(
editor,
[
AcceptPartialCopilotSuggestion,
AcceptInlineCompletion,
AcceptPartialInlineCompletion,
AddSelectionAbove,
AddSelectionBelow,

View File

@@ -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("αˇ", 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, |_| {});

View File

@@ -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());
}
}

View File

@@ -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![]);

View File

@@ -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 {

View File

@@ -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

View File

@@ -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);

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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)

View File

@@ -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())),
}
}
}

View File

@@ -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(()))
}
}

View File

@@ -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(()))
}
}

View File

@@ -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"

View File

@@ -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",
});
}

View File

@@ -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};

View File

@@ -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>;
}

View File

@@ -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>;
}

View File

@@ -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),
}
}

View File

@@ -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>;
}

View File

@@ -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>;
}

View File

@@ -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>>,
}

View File

@@ -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

View File

@@ -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(

View File

@@ -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.

View File

@@ -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

View File

@@ -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(

View File

@@ -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]

View File

@@ -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)
}
}

View File

@@ -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| {

View File

@@ -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,

View File

@@ -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>;

View File

@@ -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)]

View File

@@ -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",

View File

@@ -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
})
}

View File

@@ -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()));
}
}

View File

@@ -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)
}
}

View File

@@ -1,4 +1,4 @@
use project::ContextProviderWithTasks;
use language::ContextProviderWithTasks;
use task::{TaskTemplate, TaskTemplates, VariableName};
pub(super) fn bash_task_context() -> ContextProviderWithTasks {

View File

@@ -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::*;

View File

@@ -110,9 +110,6 @@
(imaginary_literal)
] @number
(const_spec
name: (identifier) @constant)
[
(true)
(false)

View File

@@ -1,9 +0,0 @@
(
(function_declaration name: (_) @run
(#match? @run "^Test.*"))
) @go-test
(
(function_declaration name: (_) @run
(#eq? @run "main"))
) @go-main

View File

@@ -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(

View File

@@ -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,

View File

@@ -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> {

View File

@@ -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| {

View File

@@ -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 => {

View File

@@ -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))

View File

@@ -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,

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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, &reg.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<_>>();

View File

@@ -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;

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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,

View File

@@ -515,7 +515,6 @@ message ShutdownDevServer {
message RenameDevServer {
uint64 dev_server_id = 1;
string name = 2;
optional string ssh_connection_string = 3;
}
message DeleteDevServer {

View File

@@ -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,

View File

@@ -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>,

View File

@@ -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)]

View File

@@ -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"
);

View File

@@ -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

View File

@@ -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()
});

View File

@@ -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